Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions packages/bundler-utils/src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@srcset/core'
import type { QueryOptions } from './query.ts'
import type {
SrcSetModuleOptions,
SrcSetModuleGenerateOptions,
EmitImage
} from './generate.types.ts'
import {
Expand All @@ -25,15 +25,15 @@ export type * from './generate.types.ts'
* on the bundler side and make the module code.
* @param source - Image file.
* @param query - Parsed import query options.
* @param options - Bundler integration options.
* @param options - Options of the module generation.
* @param emitImage - Emits an image on the bundler side.
* @param limit - Concurrency limit of the integration.
* @returns Module code.
*/
export async function generateSrcSetModule(
source: ImageSource,
query: QueryOptions,
options: SrcSetModuleOptions,
options: SrcSetModuleGenerateOptions,
emitImage: EmitImage,
limit?: LimitFunction
) {
Expand Down Expand Up @@ -86,5 +86,10 @@ export async function generateSrcSetModule(
})
}

return createModuleString(select, srcSet, placeholder)
return createModuleString({
select,
srcSet,
placeholder,
typescript: options.typescript
})
}
13 changes: 13 additions & 0 deletions packages/bundler-utils/src/generate.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ export interface SrcSetModuleOptions extends Omit<SrcSetGeneratorOptions, 'limit
select?: SrcSetEntrySelect
}

/**
* Options of the module generation: the integration options plus what
* only the generation itself needs.
*/
export interface SrcSetModuleGenerateOptions extends SrcSetModuleOptions {
/**
* Generate typescript instead of javascript: the variant formats are
* narrowed with `as const`, so the entries stay assignable to `SrcSetEntry`.
* Bundler integrations never need it - their modules are not written to disk.
*/
typescript?: boolean
}

/**
* Emits an image on the bundler side.
* @param image - Image variant.
Expand Down
84 changes: 47 additions & 37 deletions packages/bundler-utils/src/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,113 +32,123 @@ describe('bundler-utils', () => {
describe('module', () => {
describe('createModuleString', () => {
it('should select default variant by format and width', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
format: 'webp',
width: 320
},
[createEntry('jpg', 320), createEntry('webp', 320), createEntry('webp', 640)]
)
srcSet: [createEntry('jpg', 320), createEntry('webp', 320), createEntry('webp', 640)]
})

expect(module).toContain('const url = (__webpack_public_path__) + "image@320w.webp";')
})

it('should select default variant by multiplier', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
format: 'jpg',
width: 0.5
},
[createEntry('jpg', 640, 1), createEntry('jpg', 320, 0.5)]
)
srcSet: [createEntry('jpg', 640, 1), createEntry('jpg', 320, 0.5)]
})

expect(module).toContain('const url = (__webpack_public_path__) + "image@320w.jpg";')
})

it('should select default variant by id', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
id: 'webp640'
},
[createEntry('jpg', 320), createEntry('webp', 640)]
)
srcSet: [createEntry('jpg', 320), createEntry('webp', 640)]
})

expect(module).toContain('const url = (__webpack_public_path__) + "image@640w.webp";')
})

it('should fall back to first variant', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
format: 'avif',
width: 5000
},
[createEntry('jpg', 320), createEntry('webp', 640)]
)
srcSet: [createEntry('jpg', 320), createEntry('webp', 640)]
})

expect(module).toContain('const url = (__webpack_public_path__) + "image@320w.jpg";')
})

it('should create empty module without variants', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
format: 'jpg',
width: 640
},
[]
)
srcSet: []
})

expect(module).toContain("const url = '';")
expect(module).toContain('const src = null;')
expect(module).toContain('export const srcSet = [];')
})

it('should reuse url and src references for default variant', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
format: 'jpg',
width: 320
},
[createEntry('jpg', 320), createEntry('webp', 320)]
)
srcSet: [createEntry('jpg', 320), createEntry('webp', 320)]
})

expect(module).toContain('url: url')
expect(module).toContain('export const srcSet = [src, {')
expect(module).toContain('"jpg320": url')
})

it('should emit placeholder export', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
format: 'jpg',
width: 320
},
[createEntry('jpg', 320)],
'data:image/webp;base64,abc'
)
srcSet: [createEntry('jpg', 320)],
placeholder: 'data:image/webp;base64,abc'
})

expect(module).toContain('export const placeholder = "data:image/webp;base64,abc";')
})

it('should narrow the variant format for a typescript module', () => {
const module = createModuleString({
select: {},
srcSet: [createEntry('jpg', 320)],
typescript: true
})

expect(module).toContain('format: "jpg" as const,')
})

it('should emit undefined placeholder without data-url', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
format: 'jpg',
width: 320
},
[createEntry('jpg', 320)]
)
srcSet: [createEntry('jpg', 320)]
})

expect(module).toContain('export const placeholder = undefined;')
})

it('should map ids to urls', () => {
const module = createModuleString(
{
const module = createModuleString({
select: {
format: 'jpg',
width: 320
},
[createEntry('jpg', 320), createEntry('webp', 640)]
)
srcSet: [createEntry('jpg', 320), createEntry('webp', 640)]
})

expect(module).toContain('"webp640": (__webpack_public_path__) + "image@640w.webp"')
})
Expand Down
65 changes: 52 additions & 13 deletions packages/bundler-utils/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const emptyUrlExpression = "''"
* @returns JS expression string.
*/
function toUrlExpression(url: SrcSetImagePaths) {
if (url.urlExpression) {
return url.urlExpression
}

if (url.publicPath !== null) {
return JSON.stringify(url.publicPath)
}
Expand Down Expand Up @@ -68,31 +72,66 @@ function findDefaultIndex(select: SrcSetEntrySelect, srcSet: SrcSetModuleEntry[]
return index
}

function createEntryString({
id,
format,
type,
width,
height
}: SrcSetModuleEntry, urlString: string) {
function createEntryString(
{
id,
format,
type,
width,
height
}: SrcSetModuleEntry,
urlString: string,
typescript: boolean
) {
// Without the assertion the format of a typescript module widens to `string`,
// and the entry stops being assignable to `SrcSetEntry`.
const formatString = typescript ? `${JSON.stringify(format)} as const` : JSON.stringify(format)

return `{
id: ${JSON.stringify(id)},
format: ${JSON.stringify(format)},
format: ${formatString},
type: ${JSON.stringify(type)},
width: ${String(width)},
height: ${String(height)},
url: ${urlString}
}`
}

/**
* Options of the module code generation.
*/
export interface ModuleStringOptions {
/**
* Selection of the image variant for the default export.
*/
select: SrcSetEntrySelect
/**
* Generated image variant entries.
*/
srcSet: SrcSetModuleEntry[]
/**
* Data-url of the placeholder variant, falsy to emit `undefined`.
*/
placeholder?: string | false
/**
* Generate typescript: the variant formats are narrowed with `as const`,
* so the entries stay assignable to `SrcSetEntry`.
*/
typescript?: boolean
}

/**
* Create ES module code for the image import.
* @param select - Selection of the image variant for the default export.
* @param srcSet - Generated image variant entries.
* @param placeholder - Data-url of the placeholder variant, falsy to emit `undefined`.
* @param options - Options of the generation.
* @returns Module code.
*/
export function createModuleString(select: SrcSetEntrySelect, srcSet: SrcSetModuleEntry[], placeholder?: string | false) {
export function createModuleString(options: ModuleStringOptions) {
const {
select,
srcSet,
placeholder,
typescript = false
} = options
const defaultIndex = findDefaultIndex(select, srcSet)
const urlExpressions = srcSet.map(entry => toUrlExpression(entry.url))
const urlExpression = defaultIndex < 0 ? emptyUrlExpression : urlExpressions[defaultIndex]
Expand All @@ -103,7 +142,7 @@ export function createModuleString(select: SrcSetEntrySelect, srcSet: SrcSetModu
srcSet.forEach((entry, index) => {
const isDefault = index === defaultIndex
const urlString = isDefault ? 'url' : urlExpressions[index]
const entryString = createEntryString(entry, urlString)
const entryString = createEntryString(entry, urlString, typescript)

if (isDefault) {
srcString = entryString
Expand Down
5 changes: 5 additions & 0 deletions packages/bundler-utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ export interface SrcSetImagePaths {
* e.g. `__webpack_public_path__` of webpack.
*/
publicPathExpression?: string
/**
* JS expression of the whole url, when it is not a path at all,
* e.g. an identifier the generated module imports the image with.
*/
urlExpression?: string
}
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"test": "run -p lint test:unit test:types"
},
"dependencies": {
"@srcset/bundler-utils": "workspace:^",
"@srcset/core": "workspace:^",
"argue-cli": "^3.1.0",
"tinyglobby": "^0.2.10"
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/args.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ describe('cli', () => {

expect(parseCliArgs().help).toBe(true)
})

it('should read the module format without validating it', () => {
setArgs('--module', 'typescript')

expect(parseCliArgs().module).toBe('typescript')
})
})
})
})
5 changes: 5 additions & 0 deletions packages/cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const usage = `srcset [...sources] [...options]
--skip-optimization Do not optimize output images.
--no-scaling-up Do not generate images larger than the source.
--dest, -d Destination directory.
--module Generate an image module: ts, js, ts-dir or js-dir.
--config, -c Config file path. Defaults to the \`srcset.config.js\` lookup.
--concurrency Concurrency limit.
`
Expand All @@ -34,6 +35,7 @@ export interface CliArgs {
skipOptimization: boolean | undefined
scalingUp: boolean | undefined
dest: string | undefined
module: string | undefined
config: string | undefined
concurrency: number | undefined
}
Expand All @@ -52,6 +54,7 @@ export function parseCliArgs(): CliArgs {
skipOptimization,
scalingUp,
dest,
module: moduleFormat,
config,
concurrency
} = readOptions(
Expand All @@ -63,6 +66,7 @@ export function parseCliArgs(): CliArgs {
flag(autocase('skipOptimization')),
flag(autocase('scalingUp')),
option(alias('dest', 'd'), String),
option('module', String),
option(alias('config', 'c'), String),
option('concurrency', Number)
)
Expand Down Expand Up @@ -94,6 +98,7 @@ export function parseCliArgs(): CliArgs {
skipOptimization,
scalingUp,
dest,
module: moduleFormat,
config,
concurrency
}
Expand Down
Loading