Skip to content

Commit bd7226e

Browse files
committed
Unify plugin generated assets folder.
1 parent 1bb161d commit bd7226e

9 files changed

Lines changed: 35 additions & 39 deletions

File tree

‎frontend/purestack.studio/guides/content/image-optimization.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ For an eligible image, the plugin generates a `<picture>` with AVIF and WebP sou
6262

6363
The plugin preserves `alt`, classes, IDs, styles, links around images, and author-supplied loading attributes. It fills missing `width` and `height` attributes from the image's oriented aspect ratio so the browser can reserve space. If you supply one dimension, the other is derived; if you supply both, both are kept.
6464

65-
Generated images are written under `/_assets/images/`, with your site's `basePath` applied automatically. Filenames include a hash of the encoded bytes. Shared variants are reused across pages, and generated files are removed when no page references them. Original assets remain available at their usual URLs.
65+
Generated images are written under `/assets/images/`, with your site's `basePath` applied automatically. Filenames include a hash of the encoded bytes. Shared variants are reused across pages, and generated files are removed when no page references them. Original assets remain available at their usual URLs.
6666

6767
Widths are sorted, deduplicated, and capped at the source image's dimensions. A 700-pixel-wide source with widths `[480, 960, 1440]` produces 480- and 700-pixel candidates for each format. The plugin never enlarges a source image.
6868

‎packages/ts-common/src/site-assets.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface PageAssetInput {
2222

2323
/** Each build/session owns its processors; core handles generated files and invalidation. */
2424
export interface SiteAssets {
25-
/** A unique path segment under /_assets/. */
25+
/** A unique path segment under /assets/. */
2626
namespace: string
2727
/** Ordered within a page; different pages may run concurrently. Input methods expire when this call completes. */
2828
processPage: (input: PageAssetInput) => void | Promise<void>

‎packages/ts-images/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use `package#exportName` to select named plugins from installed packages.
5252

5353
The plugin produces `<picture>` sources for the configured modern formats and
5454
an `<img>` with a JPEG/PNG fallback, `srcset`, `sizes`, and missing dimensions.
55-
Generated URLs live under `/_assets/images/`, with the site's base path applied
55+
Generated URLs live under `/assets/images/`, with the site's base path applied
5656
by PureStack. Filenames contain the hash of the **encoded output bytes**.
5757
Widths are deduplicated, sorted, and capped at the source's oriented dimensions.
5858
If `formats: []` is selected, only the responsive fallback is emitted, without a

‎packages/ts-ssg/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ The factory creates one processor per build/session. A page awaits each processo
453453

454454
`input.document` is the rendered DOM, including component output. `await input.resolveSource(element, url)` resolves local assets using shared-content provenance and locale fallback, records source dependencies, and returns `{ relPath, absPath, publicPath }`. It returns `undefined` for external URLs. Sources must remain inside the content root. `input.dependOn(relPath)` records additional content-relative dependencies, including missing files that may appear later.
455455

456-
`await input.emit(fileName, bytes)` writes a `Uint8Array` under `/_assets/<namespace>/` and returns an encoded root-relative URL. Use immutable, content-addressed filenames: emitting different bytes under the same name is an error. Core applies the site's base path during URL rewriting. Await all emission work before `processPage` returns, and inspect template fragments explicitly when relevant; `querySelectorAll` does not traverse their content.
456+
`await input.emit(fileName, bytes)` writes a `Uint8Array` under `/assets/<namespace>/` and returns an encoded root-relative URL. Use immutable, content-addressed filenames: emitting different bytes under the same name is an error. Core applies the site's base path during URL rewriting. Await all emission work before `processPage` returns, and inspect template fragments explicitly when relevant; `querySelectorAll` does not traverse their content.
457457

458458
Core verifies existing output bytes on first use in each build/session. Repeated emissions reuse successful verification while file identity, size, and modification/change timestamps remain unchanged; changed files are checked again and repaired when necessary. Different-sized files are replaced without reading their obsolete contents. Symlink checks still run for every emission, and pruning or a new build clears the verification state.
459459

‎packages/ts-ssg/src/build/assets.test.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -554,10 +554,6 @@ describe('static assets', () => {
554554
)
555555
expect(runtimeFiles).toContain('chunks')
556556
expect(runtimeFiles.some((file) => file.startsWith('regor.'))).toBe(true)
557-
await expect(
558-
fs.stat(path.join(outDir, '_assets', 'runtime')),
559-
).rejects.toMatchObject({ code: 'ENOENT' })
560-
561557
await expect(
562558
copyStaticAsset(
563559
root,

‎packages/ts-ssg/src/build/page-assets.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ describe('page asset source tracking', () => {
137137

138138
const bytes = Buffer.from('First')
139139
const output = (context: BuildContext) =>
140-
path.join(context.config.outDir, '_assets', 'test', 'shared.txt')
140+
path.join(context.config.outDir, 'assets', 'test', 'shared.txt')
141141
async function seed(value = bytes) {
142142
const file = output(context)
143143
await fs.mkdir(path.dirname(file), { recursive: true })

‎packages/ts-ssg/src/build/page-assets.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ function reference(value: GeneratedAssetReference): GeneratedAssetReference {
4444
if (!validNamespace(value.namespace))
4545
throw new Error('Invalid generated asset namespace.')
4646
const relPath = relativeFile(value.path)
47-
if (!relPath.startsWith(`_assets/${value.namespace}/`))
47+
if (!relPath.startsWith(`assets/${value.namespace}/`))
4848
throw new Error('Generated output escapes its namespace.')
4949
return { namespace: value.namespace, path: relPath }
5050
}
@@ -231,7 +231,7 @@ export class PageAssetManager {
231231
emit: async (name, data) => {
232232
const file = reference({
233233
namespace: processor.namespace,
234-
path: `_assets/${processor.namespace}/${relativeFile(name)}`,
234+
path: `assets/${processor.namespace}/${relativeFile(name)}`,
235235
})
236236
if (
237237
!collected.outputs.some((output) => output.path === file.path)

‎packages/ts-ssg/src/plugins/assets.test.ts‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -229,15 +229,15 @@ describe('generic page assets', () => {
229229
await builder.renderByUrlPath('/a/')
230230
expect(
231231
await fs.readFile(
232-
path.join(outDir, '_assets', 'files', 'shared.txt'),
232+
path.join(outDir, 'assets', 'files', 'shared.txt'),
233233
'utf8',
234234
),
235235
).toBe('Shared')
236236
release?.()
237237
expect(await pending).toBe(true)
238238
expect(
239239
(await readManifest(outDir))?.pageAssets?.['c.md'].outputs[0].path,
240-
).toBe('_assets/files/shared.txt')
240+
).toBe('assets/files/shared.txt')
241241
})
242242

243243
it('keeps generated files leased to concurrent renders and marks an in-flight dependent dirty', async () => {
@@ -312,7 +312,7 @@ describe('generic page assets', () => {
312312
input.options = { ...input.options, writeErrorPages: true }
313313
await buildSite(input)
314314
await expect(
315-
fs.stat(path.join(outDir, '_assets', 'files', 'unused.txt')),
315+
fs.stat(path.join(outDir, 'assets', 'files', 'unused.txt')),
316316
).rejects.toMatchObject({ code: 'ENOENT' })
317317
expect((await readManifest(outDir))?.pageAssets?.['a.md']).toEqual({
318318
dependencies: ['source.txt'],
@@ -340,7 +340,7 @@ describe('generic page assets', () => {
340340
await buildSite(input)
341341
const state = (await readManifest(outDir))?.pageAssets?.['a.md']
342342
expect(state).toEqual({ dependencies: ['source.txt'], outputs: [] })
343-
expect(await fs.readdir(path.join(outDir, '_assets', 'files'))).toEqual([])
343+
expect(await fs.readdir(path.join(outDir, 'assets', 'files'))).toEqual([])
344344
expect(
345345
await fs.readFile(path.join(outDir, 'a', 'index.html'), 'utf8'),
346346
).toContain('Written hook failed')
@@ -357,20 +357,20 @@ describe('generic page assets', () => {
357357
await buildSite(input)
358358
expect(
359359
await fs.readFile(path.join(outDir, 'a', 'index.html'), 'utf8'),
360-
).toContain('/_assets/files/a%20%23.txt')
361-
await fs.mkdir(path.join(contentDir, '_assets', 'files'), {
360+
).toContain('/assets/files/a%20%23.txt')
361+
await fs.mkdir(path.join(contentDir, 'assets', 'files'), {
362362
recursive: true,
363363
})
364364
await fs.writeFile(
365-
path.join(contentDir, '_assets', 'files', 'a #.txt'),
365+
path.join(contentDir, 'assets', 'files', 'a #.txt'),
366366
'Source',
367367
)
368368
await expect(buildSite(input)).rejects.toThrow(
369369
/conflicts with (?:a source output|a generated asset)/,
370370
)
371371
expect(
372372
await fs.readFile(
373-
path.join(contentDir, '_assets', 'files', 'a #.txt'),
373+
path.join(contentDir, 'assets', 'files', 'a #.txt'),
374374
'utf8',
375375
),
376376
).toBe('Source')
@@ -383,7 +383,7 @@ describe('generic page assets', () => {
383383
const { input, contentDir } = await fixture(async (input) => {
384384
if (!scriptUrl) throw new Error('Expected script URL')
385385
await input.emit(
386-
scriptUrl.replace('/_assets/files/', ''),
386+
scriptUrl.replace('/assets/files/', ''),
387387
Buffer.from('Generated'),
388388
)
389389
})
@@ -400,13 +400,13 @@ describe('generic page assets', () => {
400400
})
401401
await fs.writeFile(
402402
path.join(contentDir, 'a.md'),
403-
'<PageScript src="./_assets/files/script.tS"/>',
403+
'<PageScript src="./assets/files/script.tS"/>',
404404
)
405-
await fs.mkdir(path.join(contentDir, '_assets', 'files'), {
405+
await fs.mkdir(path.join(contentDir, 'assets', 'files'), {
406406
recursive: true,
407407
})
408408
await fs.writeFile(
409-
path.join(contentDir, '_assets', 'files', 'script.tS'),
409+
path.join(contentDir, 'assets', 'files', 'script.tS'),
410410
'export const value = 1',
411411
)
412412
await expect(buildSite(input)).rejects.toThrow(
@@ -422,23 +422,23 @@ describe('generic page assets', () => {
422422
})
423423
input.siteConfig = { ...input.siteConfig, scripts: { cacheBusting: false } }
424424
input.options?.plugins?.push(componentsPlugin())
425-
await fs.mkdir(path.join(contentDir, '_assets', 'files'), {
425+
await fs.mkdir(path.join(contentDir, 'assets', 'files'), {
426426
recursive: true,
427427
})
428428
await fs.writeFile(
429-
path.join(contentDir, '_assets', 'files', 'script.ts'),
429+
path.join(contentDir, 'assets', 'files', 'script.ts'),
430430
'export const value = 1',
431431
)
432432
await fs.writeFile(
433433
path.join(contentDir, 'b.md'),
434-
'<PageScript src="./_assets/files/script.ts"/>',
434+
'<PageScript src="./assets/files/script.ts"/>',
435435
)
436436
await expect(buildSite(input)).rejects.toThrow(
437437
'conflicts with a generated asset',
438438
)
439439
expect(
440440
await fs.readFile(
441-
path.join(outDir, '_assets', 'files', 'script.js'),
441+
path.join(outDir, 'assets', 'files', 'script.js'),
442442
'utf8',
443443
),
444444
).toBe('Generated')
@@ -475,7 +475,7 @@ describe('generic page assets', () => {
475475
'input is closed',
476476
)
477477
await expect(
478-
fs.stat(path.join(outDir, '_assets', 'files', 'late.txt')),
478+
fs.stat(path.join(outDir, 'assets', 'files', 'late.txt')),
479479
).rejects.toMatchObject({ code: 'ENOENT' })
480480
})
481481

@@ -487,7 +487,7 @@ describe('generic page assets', () => {
487487
'must await all source resolution and emission work',
488488
)
489489
await expect(
490-
fs.stat(path.join(outDir, '_assets', 'files', 'detached.txt')),
490+
fs.stat(path.join(outDir, 'assets', 'files', 'detached.txt')),
491491
).rejects.toMatchObject({ code: 'ENOENT' })
492492
})
493493

‎packages/ts-ssg/src/plugins/images.test.ts‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ describe('optional images plugin', () => {
8282
fs.readFile(path.join(outDir, file), 'utf8')
8383
async function generated(outDir: string) {
8484
return (
85-
await fs.readdir(path.join(outDir, '_assets', 'images')).catch(() => [])
85+
await fs.readdir(path.join(outDir, 'assets', 'images')).catch(() => [])
8686
).sort()
8787
}
8888

@@ -95,7 +95,7 @@ describe('optional images plugin', () => {
9595
const expectedFiles = await generated(outDir)
9696
const expectedBytes = await Promise.all(
9797
expectedFiles.map((file) =>
98-
fs.readFile(path.join(outDir, '_assets', 'images', file)),
98+
fs.readFile(path.join(outDir, 'assets', 'images', file)),
9999
),
100100
)
101101
// A site-only package exercises fallback resolution without competing with
@@ -157,7 +157,7 @@ describe('optional images plugin', () => {
157157
expect(
158158
await Promise.all(
159159
expectedFiles.map((file) =>
160-
fs.readFile(path.join(outDir, '_assets', 'images', file)),
160+
fs.readFile(path.join(outDir, 'assets', 'images', file)),
161161
),
162162
),
163163
).toEqual(expectedBytes)
@@ -202,7 +202,7 @@ describe('optional images plugin', () => {
202202
const image = document.querySelector('picture > img')
203203
expect(image?.getAttribute('alt')).toBe('A dashboard')
204204
expect(image?.getAttribute('src')).toMatch(
205-
/^\/docs\/_assets\/images\/[a-f0-9]{64}\.64\.png$/,
205+
/^\/docs\/assets\/images\/[a-f0-9]{64}\.64\.png$/,
206206
)
207207
expect(image?.getAttribute('width')).toBe('64')
208208
expect(image?.getAttribute('height')).toBe('32')
@@ -221,7 +221,7 @@ describe('optional images plugin', () => {
221221
})
222222
for (const file of await generated(outDir)) {
223223
const metadata = await sharp(
224-
await fs.readFile(path.join(outDir, '_assets', 'images', file)),
224+
await fs.readFile(path.join(outDir, 'assets', 'images', file)),
225225
).metadata()
226226
expect(metadata.width).toBeLessThanOrEqual(64)
227227
expect(
@@ -251,7 +251,7 @@ describe('optional images plugin', () => {
251251
).toBe('Photo')
252252
expect(
253253
document.querySelector('picture > img')?.getAttribute('src'),
254-
).toMatch(/^\/docs\/_assets\/images\//)
254+
).toMatch(/^\/docs\/assets\/images\//)
255255
},
256256
)
257257
})
@@ -325,7 +325,7 @@ describe('optional images plugin', () => {
325325
await runInDom(output, (document) => {
326326
expect(
327327
document.querySelector('img[alt="Header"]')?.getAttribute('src'),
328-
).toContain('/docs/_assets/images/')
328+
).toContain('/docs/assets/images/')
329329
expect(
330330
(
331331
document.querySelector('template') as HTMLTemplateElement
@@ -476,7 +476,7 @@ describe('optional images plugin', () => {
476476
expect(document.querySelectorAll('img[srcset]')).toHaveLength(2)
477477
expect(
478478
document.querySelector('img[alt="Hook"]')?.getAttribute('src'),
479-
).toContain('/docs/_assets/images/')
479+
).toContain('/docs/assets/images/')
480480
})
481481
})
482482

@@ -508,7 +508,7 @@ describe('optional images plugin', () => {
508508
expect(
509509
(
510510
await sharp(
511-
await fs.readFile(path.join(secondOutput, '_assets', 'images', file)),
511+
await fs.readFile(path.join(secondOutput, 'assets', 'images', file)),
512512
).metadata()
513513
).width,
514514
).toBe(24)
@@ -611,7 +611,7 @@ describe('optional images plugin', () => {
611611
expect(response.status).toBe(200)
612612
const output = await response.text()
613613
const urls =
614-
output.match(/\/docs\/_assets\/images\/[a-f0-9]+\.\d+\.(?:avif|webp)/g) ??
614+
output.match(/\/docs\/assets\/images\/[a-f0-9]+\.\d+\.(?:avif|webp)/g) ??
615615
[]
616616
expect(urls.length).toBeGreaterThan(0)
617617
for (const extension of ['avif', 'webp']) {

0 commit comments

Comments
 (0)