From 828e06800e0464c672452e546c4d2cc21482d1fc Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sat, 5 Sep 2026 21:16:59 -0700 Subject: [PATCH] Centralize file conventions across discovery, watch, and bundling --- index.js | 29 ++------ lib/build-esbuild/index.js | 17 ++--- lib/build-static/index.js | 3 +- lib/file-conventions.js | 114 ++++++++++++++++++++++++++++++ lib/file-conventions.test.js | 62 +++++++++++++++++ lib/identify-pages.js | 123 ++++----------------------------- test-cases/watch/index.test.js | 25 +++++++ 7 files changed, 225 insertions(+), 148 deletions(-) create mode 100644 lib/file-conventions.js create mode 100644 lib/file-conventions.test.js diff --git a/index.js b/index.js index 2c65643..6a19dfb 100644 --- a/index.js +++ b/index.js @@ -36,6 +36,7 @@ import { find } from '@11ty/dependency-tree-typescript' import { assertInsideDest } from './lib/helpers/path.js' import { getCopyGlob } from './lib/build-static/index.js' import { getCopyDirs } from './lib/build-copy/index.js' +import { classifyFile, isProcessedFile, globalBundleAssets, pageBundleAssets, layoutBundleAssets } from './lib/file-conventions.js' import { builder } from './lib/builder.js' import { buildEsbuildWatch } from './lib/build-esbuild/index.js' import { buildPages } from './lib/build-pages/index.js' @@ -46,12 +47,9 @@ import { esbuildSettingsNames, markdownItSettingsNames, domstackManifestSettingsNames, - pageClientNames, layoutClientSuffixs, globalClientNames, globalStyleNames, - pageStyleName, - pageWorkerSuffixs, serviceWorkerNames, } from './lib/identify-pages.js' import { ensureDest } from './lib/helpers/ensure-dest.js' @@ -347,7 +345,7 @@ export class DomStack { ignored: (filePath, stats) => { return ( anymatch(filePath) || - Boolean((stats?.isFile() && !/\.(js|mjs|cjs|ts|mts|cts|css|html|md)$/.test(filePath))) + Boolean((stats?.isFile() && !isProcessedFile(filePath))) ) }, persistent: true, @@ -456,16 +454,7 @@ ${siteData.errors.map(err => ` ${err.message}`).join('\n')}`) const changedDir = relative(this.#src, dirname(changedPath)) // Check if this is an esbuild entry point by basename pattern - const isEsbuildEntry = ( - pageClientNames.includes(changedBasename) || - layoutClientSuffixs.some(s => changedBasename.endsWith(s)) || - changedBasename.endsWith(layoutStyleSuffix) || - pageWorkerSuffixs.some(s => changedBasename.endsWith(s)) || - serviceWorkerNames.includes(changedBasename) || - globalClientNames.includes(changedBasename) || - globalStyleNames.includes(changedBasename) || - changedBasename === pageStyleName - ) + const isEsbuildEntry = classifyFile(changedBasename)?.bundleScope if (isEsbuildEntry) { this.#logger.info(`"${changedBasename}" ${event}, restarting esbuild...`) @@ -798,19 +787,13 @@ ${siteData.errors.map(err => ` ${err.message}`).join('\n')}`) // esbuildEntryPoints: absolute filepaths of all esbuild entry points const esbuildEntryPoints = /** @type {Set} */ (new Set()) - if (siteData.globalClient) esbuildEntryPoints.add(resolve(siteData.globalClient.filepath)) - if (siteData.globalStyle) esbuildEntryPoints.add(resolve(siteData.globalStyle.filepath)) + for (const asset of globalBundleAssets(siteData)) esbuildEntryPoints.add(resolve(asset.filepath)) if (siteData.serviceWorker) esbuildEntryPoints.add(resolve(siteData.serviceWorker.filepath)) for (const page of siteData.pages) { - if (page.clientBundle) esbuildEntryPoints.add(resolve(page.clientBundle.filepath)) - if (page.pageStyle) esbuildEntryPoints.add(resolve(page.pageStyle.filepath)) - if (page.workers) { - for (const w of Object.values(page.workers)) esbuildEntryPoints.add(resolve(w.filepath)) - } + for (const asset of pageBundleAssets(page)) esbuildEntryPoints.add(resolve(asset.filepath)) } for (const layout of Object.values(siteData.layouts)) { - if (layout.layoutClient) esbuildEntryPoints.add(resolve(layout.layoutClient.filepath)) - if (layout.layoutStyle) esbuildEntryPoints.add(resolve(layout.layoutStyle.filepath)) + for (const asset of layoutBundleAssets(layout)) esbuildEntryPoints.add(resolve(asset.filepath)) } this.#layoutDepMap = layoutDepMap diff --git a/lib/build-esbuild/index.js b/lib/build-esbuild/index.js index 30305b3..fb8f527 100644 --- a/lib/build-esbuild/index.js +++ b/lib/build-esbuild/index.js @@ -6,6 +6,7 @@ import { writeFile } from 'fs/promises' import { join, relative, basename, resolve, extname } from 'path' import esbuild from 'esbuild' +import { globalBundleAssets, pageBundleAssets, layoutBundleAssets } from '../file-conventions.js' import { resolveVars } from '../build-pages/resolve-vars.js' import { createDomstackManifestRecord, @@ -168,9 +169,7 @@ function updateOutputFileInfo (outputMap, fileInfo) { * @returns {Promise} */ async function createBrowserBuildOpts (src, dest, siteData, opts, modeOpts = {}) { - const entryPoints = [] - if (siteData.globalClient) entryPoints.push(join(src, siteData.globalClient.relname)) - if (siteData.globalStyle) entryPoints.push(join(src, siteData.globalStyle.relname)) + const entryPoints = /** @type {(string | { in: string, out: string })[]} */ (globalBundleAssets(siteData).map(asset => join(src, asset.relname))) if (siteData.defaultLayout) { entryPoints.push( @@ -180,19 +179,11 @@ async function createBrowserBuildOpts (src, dest, siteData, opts, modeOpts = {}) } for (const page of siteData.pages) { - if (page.clientBundle) entryPoints.push(join(src, page.clientBundle.relname)) - if (page.pageStyle) entryPoints.push(join(src, page.pageStyle.relname)) - - if (page.workers) { - for (const workerFile of Object.values(page.workers)) { - entryPoints.push(join(src, workerFile.relname)) - } - } + for (const asset of pageBundleAssets(page)) entryPoints.push(join(src, asset.relname)) } for (const layout of Object.values(siteData.layouts)) { - if (layout.layoutClient) entryPoints.push(join(src, layout.layoutClient.relname)) - if (layout.layoutStyle) entryPoints.push(join(src, layout.layoutStyle.relname)) + for (const asset of layoutBundleAssets(layout)) entryPoints.push(join(src, asset.relname)) } const browserVars = await resolveVars({ diff --git a/lib/build-static/index.js b/lib/build-static/index.js index 21bf15d..3bbdb0e 100644 --- a/lib/build-static/index.js +++ b/lib/build-static/index.js @@ -1,6 +1,7 @@ /** * @import { BuildStep, BuildStepResult } from '../builder.js' */ +import { processedExtensions } from '../file-conventions.js' import { copy } from 'cpx2' import { createCopiedDomstackManifestRecords } from '../helpers/cpx2-report.js' @@ -22,7 +23,7 @@ import { createCopiedDomstackManifestRecords } from '../helpers/cpx2-report.js' */ export function getCopyGlob (src) { // Always ignore files we typically process. Otherwise it gets really confusing. - return `${src}/**/!(*.ts|*.tsx|*.mts|*.cts|*.js|*.jsx|*.cjs|*.mjs|*.css|*.html|*.md)` + return `${src}/**/!(${processedExtensions.map(ext => `*.${ext}`).join('|')})` } /** diff --git a/lib/file-conventions.js b/lib/file-conventions.js new file mode 100644 index 0000000..62d68a2 --- /dev/null +++ b/lib/file-conventions.js @@ -0,0 +1,114 @@ +/** + * @import { PageInfo, Layout, PageFileAsset } from './identify-pages.js' + * @import { SiteData } from './builder.js' + * @typedef {'page' | 'layout' | 'global' | 'service-worker' | null} BundleScope + * @typedef {'page' | 'layout' | 'pages-file' | 'template' | 'full' | 'pages' | 'markdown' | 'manifest' | 'bundle'} ChangeKind + * @typedef {{ names: string[], suffixes: string[], draftNames: string[], bundleScope: BundleScope, change: ChangeKind }} FileConvention + */ +import { extname } from 'node:path' +import { nodeHasTS } from './helpers/has-ts.js' + +const javascriptExtensions = ['js', 'mjs', 'cjs'] +const typescriptExtensions = ['ts', 'mts', 'cts'] +const clientExtensions = ['tsx', ...typescriptExtensions, 'jsx', ...javascriptExtensions] +export const processedExtensions = [...clientExtensions, 'css', 'html', 'md'] +export const layoutStyleSuffix = '.layout.css' +export const pageStyleName = 'style.css' + +/** + * @param {string[]} names + * @param {ChangeKind} change + * @param {BundleScope} [bundleScope] + * @param {string[]} [suffixes] + * @param {string[]} [draftNames] + * @returns {FileConvention} + */ +function convention (names, change, bundleScope = null, suffixes = [], draftNames = []) { + return { names, suffixes, draftNames, bundleScope, change } +} + +/** @param {boolean} [supportsTypeScript] */ +export function createFileConventions (supportsTypeScript = nodeHasTS) { + const serverExtensions = supportsTypeScript ? [...typescriptExtensions, ...javascriptExtensions] : javascriptExtensions + const names = (/** @type {string} */ stem, extensions = serverExtensions) => extensions.map(ext => `${stem}.${ext}`) + return { + page: convention(names('page'), 'page', null, [], names('page.draft')), + htmlPage: convention(['page.html'], 'page', null, [], ['page.draft.html']), + markdownPage: convention(['page.md'], 'page', null, [], ['page.draft.md']), + readmePage: convention(['README.md'], 'page', null, [], ['README.draft.md']), + pageVars: convention(names('page.vars'), 'page'), + pageClient: convention(names('client', clientExtensions), 'bundle', 'page'), + pageStyle: convention([pageStyleName], 'bundle', 'page'), + pageWorker: convention([], 'bundle', 'page', names('.worker')), + layout: convention([], 'layout', null, names('.layout')), + layoutClient: convention([], 'bundle', 'layout', names('.layout.client', clientExtensions)), + layoutStyle: convention([], 'bundle', 'layout', [layoutStyleSuffix]), + template: convention([], 'template', null, names('.template')), + pages: convention([], 'pages-file', null, names('.pages')), + globalStyle: convention(['global.css', 'global.style.css'], 'bundle', 'global'), + globalClient: convention(names('global.client', clientExtensions), 'bundle', 'global'), + serviceWorker: convention(names('service-worker'), 'bundle', 'service-worker'), + globalVars: convention(names('global.vars'), 'full'), + globalData: convention(names('global.data'), 'pages'), + esbuildSettings: convention(names('esbuild.settings'), 'full'), + markdownSettings: convention(names('markdown-it.settings'), 'markdown'), + manifestSettings: convention(names('domstack-manifest.settings'), 'manifest'), + } +} + +export const fileConventions = createFileConventions() + +/** + * Classify a basename; dynamic dependency membership belongs to the watch planner. + * @param {string} name + * @param {ReturnType} [conventions] + */ +export function classifyFile (name, conventions = fileConventions) { + return Object.values(conventions).find(rule => + rule.names.includes(name) || rule.draftNames.includes(name) || rule.suffixes.some(suffix => name.endsWith(suffix)) + ) +} + +/** @param {string} filepath */ +export function isProcessedFile (filepath) { + return processedExtensions.includes(extname(filepath).slice(1)) +} + +// Preserve existing discovery exports and filename priority for downstream users. +export const jsPageNames = fileConventions.page.names +export const jsPageDraftNames = fileConventions.page.draftNames +export const pageVarsNames = fileConventions.pageVars.names +export const pageClientNames = fileConventions.pageClient.names +export const pageWorkerSuffixs = fileConventions.pageWorker.suffixes +export const layoutSuffixs = fileConventions.layout.suffixes +export const layoutClientSuffixs = fileConventions.layoutClient.suffixes +export const templateSuffixs = fileConventions.template.suffixes +export const pagesSuffixs = fileConventions.pages.suffixes +export const globalStyleNames = fileConventions.globalStyle.names +export const globalClientNames = fileConventions.globalClient.names +export const serviceWorkerNames = fileConventions.serviceWorker.names +export const globalVarsNames = fileConventions.globalVars.names +export const globalDataNames = fileConventions.globalData.names +export const esbuildSettingsNames = fileConventions.esbuildSettings.names +export const markdownItSettingsNames = fileConventions.markdownSettings.names +export const domstackManifestSettingsNames = fileConventions.manifestSettings.names + +/** @param {SiteData} siteData */ +export function globalBundleAssets (siteData) { + return presentAssets([siteData.globalClient, siteData.globalStyle]) +} + +/** @param {PageInfo} page */ +export function pageBundleAssets (page) { + return presentAssets([page.clientBundle, page.pageStyle, ...Object.values(page.workers ?? {})]) +} + +/** @param {Layout} layout */ +export function layoutBundleAssets (layout) { + return presentAssets([layout.layoutClient, layout.layoutStyle]) +} + +/** @param {(PageFileAsset | undefined)[]} assets */ +function presentAssets (assets) { + return assets.filter(asset => asset !== undefined) +} diff --git a/lib/file-conventions.test.js b/lib/file-conventions.test.js new file mode 100644 index 0000000..d2b2809 --- /dev/null +++ b/lib/file-conventions.test.js @@ -0,0 +1,62 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { classifyFile, createFileConventions, isProcessedFile } from './file-conventions.js' +import { getCopyGlob } from './build-static/index.js' + +test('server extensions follow runtime support while browser clients always support JSX and TypeScript', () => { + const withTypes = createFileConventions(true) + const withoutTypes = createFileConventions(false) + assert.deepEqual(withTypes.page.names, ['page.ts', 'page.mts', 'page.cts', 'page.js', 'page.mjs', 'page.cjs']) + assert.deepEqual(withoutTypes.page.names, ['page.js', 'page.mjs', 'page.cjs']) + assert.deepEqual(withTypes.page.draftNames, withTypes.page.names.map(name => name.replace('page.', 'page.draft.'))) + assert.equal(classifyFile('page.ts', withoutTypes), undefined) + assert.equal(classifyFile('counter.worker.mts', withoutTypes), undefined) + assert.equal(classifyFile('service-worker.ts', withoutTypes), undefined) + for (const extension of ['tsx', 'ts', 'mts', 'cts', 'jsx', 'js', 'mjs', 'cjs']) { + assert.equal(classifyFile(`client.${extension}`, withoutTypes)?.bundleScope, 'page') + assert.equal(classifyFile(`root.layout.client.${extension}`, withoutTypes)?.bundleScope, 'layout') + assert.equal(classifyFile(`global.client.${extension}`, withoutTypes)?.bundleScope, 'global') + } +}) + +test('file roles distinguish structural settings, output owners, and asset scopes', () => { + const cases = [ + ['page.draft.html', 'page', null], + ['README.draft.md', 'page', null], + ['root.layout.js', 'layout', null], + ['archive.pages.js', 'pages-file', null], + ['feed.template.js', 'template', null], + ['global.vars.js', 'full', null], + ['global.data.js', 'pages', null], + ['esbuild.settings.js', 'full', null], + ['markdown-it.settings.js', 'markdown', null], + ['domstack-manifest.settings.js', 'manifest', null], + ['style.css', 'bundle', 'page'], + ['counter.worker.js', 'bundle', 'page'], + ['root.layout.css', 'bundle', 'layout'], + ['global.style.css', 'bundle', 'global'], + ['service-worker.js', 'bundle', 'service-worker'], + ] + for (const [name, change, scope] of cases) { + assert.equal(typeof name, 'string') + const rule = classifyFile(/** @type {string} */ (name)) + assert.equal(rule?.change, change, name ?? '') + assert.equal(rule?.bundleScope, scope, name ?? '') + } + assert.equal(classifyFile('arbitrary.js'), undefined) + assert.equal(classifyFile('style.css.map'), undefined) +}) + +test('all convention entries are observed and excluded from static copying', () => { + const glob = getCopyGlob('/site') + for (const rule of Object.values(createFileConventions(true))) { + for (const name of [...rule.names, ...rule.draftNames, ...rule.suffixes.map(suffix => `example${suffix}`)]) { + assert.equal(isProcessedFile(name), true, name) + assert.ok(glob.includes(`*.${name.split('.').at(-1)}`), name) + } + } + assert.equal(isProcessedFile('client.tsx'), true) + assert.equal(isProcessedFile('client.jsx'), true) + assert.equal(isProcessedFile('image.png'), false) + assert.equal(isProcessedFile('client.js.map'), false) +}) diff --git a/lib/identify-pages.js b/lib/identify-pages.js index 420ecb6..4f2ee9f 100644 --- a/lib/identify-pages.js +++ b/lib/identify-pages.js @@ -7,7 +7,8 @@ import assert from 'node:assert' import { resolve, relative, join, basename } from 'path' import { pageBuilders } from './build-pages/index.js' import { DomStackDuplicatePageError, DomStackDuplicateServiceWorkerError } from './helpers/domstack-error.js' -import { nodeHasTS } from './helpers/has-ts.js' +import { fileConventions, jsPageNames, jsPageDraftNames, pageClientNames, pageWorkerSuffixs, pageVarsNames, layoutSuffixs, layoutClientSuffixs, layoutStyleSuffix, templateSuffixs, pagesSuffixs, globalStyleNames, globalClientNames, serviceWorkerNames, globalVarsNames, globalDataNames, esbuildSettingsNames, markdownItSettingsNames, domstackManifestSettingsNames } from './file-conventions.js' +export { jsPageNames, jsPageDraftNames, pageClientNames, pageWorkerSuffixs, pageVarsNames, layoutSuffixs, layoutClientSuffixs, layoutStyleSuffix, templateSuffixs, pagesSuffixs, globalStyleNames, pageStyleName, globalClientNames, serviceWorkerNames, globalVarsNames, globalDataNames, esbuildSettingsNames, markdownItSettingsNames, domstackManifestSettingsNames } from './file-conventions.js' import { computePageUrl } from './build-pages/compute-page-url.js' const __dirname = import.meta.dirname @@ -22,104 +23,6 @@ const getFirstMatch = (/** @type {{ [basename: string]: WalkerFile }} */ files, } } -export const jsPageNames = nodeHasTS - ? ['page.ts', 'page.mts', 'page.cts', 'page.js', 'page.mjs', 'page.cjs'] - : ['page.js', 'page.mjs', 'page.cjs'] - -export const jsPageDraftNames = nodeHasTS - ? ['page.draft.ts', 'page.draft.mts', 'page.draft.cts', 'page.draft.js', 'page.draft.mjs', 'page.draft.cjs'] - : ['page.draft.js', 'page.draft.mjs', 'page.draft.cjs'] - -export const pageClientNames = [ - 'client.tsx', 'client.ts', 'client.mts', 'client.cts', - 'client.jsx', 'client.js', 'client.mjs', 'client.cjs' -] - -const workerSuffix = '.worker' -const workerExtensions = nodeHasTS - ? ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'] - : ['.js', '.mjs', '.cjs'] - -export const pageWorkerSuffixs = workerExtensions.map(ext => workerSuffix + ext) - -export const pageVarsNames = nodeHasTS - ? ['page.vars.ts', 'page.vars.mts', 'page.vars.cts', - 'page.vars.js', 'page.vars.mjs', 'page.vars.cjs'] - : ['page.vars.js', 'page.vars.mjs', 'page.vars.cjs'] - -export const layoutSuffixs = nodeHasTS - ? ['.layout.ts', '.layout.mts', '.layout.cts', '.layout.js', '.layout.mjs', '.layout.cjs'] - : ['.layout.js', '.layout.mjs', '.layout.cjs'] - -export const layoutClientSuffixs = [ - '.layout.client.tsx', '.layout.client.ts', - '.layout.client.mts', '.layout.client.cts', - '.layout.client.jsx', '.layout.client.js', - '.layout.client.mjs', '.layout.client.cjs' -] - -export const layoutStyleSuffix = '.layout.css' - -export const templateSuffixs = nodeHasTS - ? ['.template.ts', '.template.mts', '.template.cts', '.template.js', '.template.mjs', '.template.cjs'] - : ['.template.js', '.template.mjs', '.template.cjs'] - -export const pagesSuffixs = nodeHasTS - ? ['.pages.ts', '.pages.mts', '.pages.cts', '.pages.js', '.pages.mjs', '.pages.cjs'] - : ['.pages.js', '.pages.mjs', '.pages.cjs'] - -export const globalStyleNames = ['global.css', 'global.style.css'] -export const pageStyleName = 'style.css' - -export const globalClientNames = [ - 'global.client.tsx', 'global.client.ts', - 'global.client.mts', 'global.client.cts', - 'global.client.jsx', 'global.client.js', - 'global.client.mjs', 'global.client.cjs' -] - -export const serviceWorkerNames = nodeHasTS - ? [ - 'service-worker.ts', 'service-worker.mts', 'service-worker.cts', - 'service-worker.js', 'service-worker.mjs', 'service-worker.cjs' - ] - : ['service-worker.js', 'service-worker.mjs', 'service-worker.cjs'] - -export const globalVarsNames = nodeHasTS - ? [ - 'global.vars.ts', 'global.vars.mts', 'global.vars.cts', - 'global.vars.js', 'global.vars.mjs', 'global.vars.cjs' - ] - : ['global.vars.js', 'global.vars.mjs', 'global.vars.cjs'] - -export const globalDataNames = nodeHasTS - ? [ - 'global.data.ts', 'global.data.mts', 'global.data.cts', - 'global.data.js', 'global.data.mjs', 'global.data.cjs' - ] - : ['global.data.js', 'global.data.mjs', 'global.data.cjs'] - -export const esbuildSettingsNames = nodeHasTS - ? [ - 'esbuild.settings.ts', 'esbuild.settings.mts', 'esbuild.settings.cts', - 'esbuild.settings.js', 'esbuild.settings.mjs', 'esbuild.settings.cjs' - ] - : ['esbuild.settings.js', 'esbuild.settings.mjs', 'esbuild.settings.cjs'] - -export const markdownItSettingsNames = nodeHasTS - ? [ - 'markdown-it.settings.ts', 'markdown-it.settings.mts', 'markdown-it.settings.cts', - 'markdown-it.settings.js', 'markdown-it.settings.mjs', 'markdown-it.settings.cjs' - ] - : ['markdown-it.settings.js', 'markdown-it.settings.mjs', 'markdown-it.settings.cjs'] - -export const domstackManifestSettingsNames = nodeHasTS - ? [ - 'domstack-manifest.settings.ts', 'domstack-manifest.settings.mts', 'domstack-manifest.settings.cts', - 'domstack-manifest.settings.js', 'domstack-manifest.settings.mjs', 'domstack-manifest.settings.cjs' - ] - : ['domstack-manifest.settings.js', 'domstack-manifest.settings.mjs', 'domstack-manifest.settings.cjs'] - /** * Shape the file walker object * @@ -308,21 +211,21 @@ export async function identifyPages (src, opts = {}) { if (jsPage) jsPage.type = 'js' /** @type {PageFile | undefined} */ const htmlPage = opts?.buildDrafts - ? files['page.html'] ?? files['page.draft.html'] - : files['page.html'] + ? getFirstMatch(files, fileConventions.htmlPage.names, fileConventions.htmlPage.draftNames) + : getFirstMatch(files, fileConventions.htmlPage.names) if (htmlPage) htmlPage.type = 'html' /** @type {PageFile | undefined} */ const pageMd = opts?.buildDrafts - ? files['page.md'] ?? files['page.draft.md'] - : files['page.md'] + ? getFirstMatch(files, fileConventions.markdownPage.names, fileConventions.markdownPage.draftNames) + : getFirstMatch(files, fileConventions.markdownPage.names) if (pageMd) pageMd.type = 'md' /** @type {PageFile | undefined} */ const readmePage = opts?.buildDrafts - ? files['README.md'] ?? files['README.draft.md'] - : files['README.md'] + ? getFirstMatch(files, fileConventions.readmePage.names, fileConventions.readmePage.draftNames) + : getFirstMatch(files, fileConventions.readmePage.names) if (readmePage) readmePage.type = 'md' - const pageStyle = files['style.css'] + const pageStyle = getFirstMatch(files, fileConventions.pageStyle.names) const clientBundle = getFirstMatch(files, pageClientNames) const pageVars = getFirstMatch(files, pageVarsNames) @@ -330,13 +233,11 @@ export async function identifyPages (src, opts = {}) { /** @type {{ [name: string]: PageFileAsset }} */ const workerFiles = {} for (const [fileName, fileInfo] of Object.entries(files)) { - const workerMatch = workerExtensions.some(ext => - fileName.endsWith(workerSuffix + ext) - ) + const workerMatch = pageWorkerSuffixs.some(suffix => fileName.endsWith(suffix)) - if (workerMatch && fileName.includes(workerSuffix)) { + if (workerMatch && fileName.includes('.worker')) { // Extract worker name (everything before .worker.{ext}) - const workerName = fileName.split(workerSuffix)[0] + const workerName = fileName.split('.worker')[0] if (workerName) { workerFiles[workerName] = fileInfo } diff --git a/test-cases/watch/index.test.js b/test-cases/watch/index.test.js index 7dcdddf..95e08cb 100644 --- a/test-cases/watch/index.test.js +++ b/test-cases/watch/index.test.js @@ -264,6 +264,31 @@ test.describe('watch', () => { assert.ok(loggerLogs.some(line => line.includes('"root.layout.js" changed:') && line.includes('index.html'))) }) + test('adding and removing a JSX client updates its page bundle', { timeout: 20_000 }, async (t) => { + const tmp = await mkdtemp(path.join(import.meta.dirname, '.tmp-jsx-client-')) + const src = path.join(tmp, 'src') + const dest = path.join(tmp, 'public') + await mkdir(src, { recursive: true }) + await writeFile(path.join(src, 'page.md'), '# JSX client\n') + const domStack = new DomStack(src, dest) + t.after(async () => { + if (domStack.watching) await domStack.stopWatching() + await rm(tmp, { recursive: true, force: true }) + }) + await domStack.watch({ serve: false }) + + const output = path.join(dest, 'index.html') + assert.ok(!(await readFile(output, 'utf8')).includes('src="./client.js"')) + + await writeFile(path.join(src, 'client.jsx'), 'console.log("JSX client entry")\n') + await settle(domStack) + assert.ok((await readFile(output, 'utf8')).includes('src="./client.js"')) + assert.ok((await readFile(path.join(dest, 'client.js'), 'utf8')).includes('JSX client entry')) + + await unlink(path.join(src, 'client.jsx')) + await settle(domStack) + assert.ok(!(await readFile(output, 'utf8')).includes('src="./client.js"')) + }) test('targets generated-page owners independently', { timeout: 30_000 }, async (t) => { const tmp = await mkdtemp(path.join(import.meta.dirname, '.tmp-generated-')) const src = path.join(tmp, 'src')