From 4b8997660f8b675d91d4887723caa7f075395d16 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sun, 13 Sep 2026 15:34:15 -0700 Subject: [PATCH 1/2] Use a canonical TypeScript default layout and language-aware eject --- bin.js | 50 +++++++++---- docs/cli/README.md | 20 ++++- docs/layouts/README.md | 4 +- lib/build-pages/page-data.js | 3 +- lib/defaults/default.root.layout.test.js | 2 +- ....root.layout.js => default.root.layout.ts} | 28 +++---- lib/defaults/load-layout.js | 31 ++++++++ lib/identify-pages.js | 2 +- package.json | 1 + scripts/test-packed-types.js | 30 +++++++- test-cases/cli-errors/eject.test.js | 74 +++++++++++++++++++ 11 files changed, 206 insertions(+), 39 deletions(-) rename lib/defaults/{default.root.layout.js => default.root.layout.ts} (75%) create mode 100644 lib/defaults/load-layout.js create mode 100644 test-cases/cli-errors/eject.test.js diff --git a/bin.js b/bin.js index 13b79105..556823ac 100755 --- a/bin.js +++ b/bin.js @@ -7,7 +7,8 @@ * @import { BsInstance } from '@domstack/sync' */ -import { readFile } from 'node:fs/promises' +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { stripTypeScriptTypes } from 'node:module' import { basename, resolve, join, relative } from 'node:path' import { parseArgs } from 'node:util' import { printHelpText } from 'argsclopts' @@ -74,6 +75,15 @@ const options = { short: 'e', help: 'eject the DOMStack default layout, style and client into the src flag directory', }, + language: { + type: 'string', + default: 'js', + help: 'language for --eject: ts or js (default: js)', + }, + yes: { + type: 'boolean', + help: 'skip confirmation for --eject', + }, watch: { type: 'boolean', short: 'w', @@ -143,10 +153,10 @@ async function run () { // Eject task if (argv['eject']) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }) + const language = argv['language'] + if (language !== 'ts' && language !== 'js') { + throw new Error('--language must be ts or js') + } const localPkg = await packageDirectory({ cwd: src }) @@ -162,9 +172,10 @@ async function run () { const relativeSrc = relative(process.cwd(), src) const relativePkg = relative(process.cwd(), localPkgJson) - const targetLayoutPath = `layouts/root.layout.${targetIsModule ? 'js' : 'mjs'}` + const extension = language === 'ts' ? (targetIsModule ? 'ts' : 'mts') : targetIsModule ? 'js' : 'mjs' + const targetLayoutPath = `layouts/root.layout.${extension}` const targetGlobalStylePath = 'globals/global.css' - const targetGlobalClientPath = `globals/global.client.${targetIsModule ? 'js' : 'mjs'}` + const targetGlobalClientPath = `globals/global.client.${language === 'ts' ? 'ts' : extension}` const tbPkgContents = await getPkg() const mineVersion = tbPkgContents?.['dependencies']?.['mine.css'] @@ -185,18 +196,31 @@ domstack eject actions: - Add fragtml@${fragtmlVersion} to ${relativePkg} - Add highlight.js@${highlightVersion} to ${relativePkg} `) - const answer = await askYesNo(rl, 'Continue?') - if (answer === false) { - console.log('No action taken. Exiting.') - process.exit(0) + if (!argv['yes']) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + let answer + try { + answer = await askYesNo(rl, 'Continue?') + } finally { + rl.close() + } + if (!answer) { + console.log('No action taken. Exiting.') + process.exit(0) + } } - const defaultLayoutPath = join(__dirname, 'lib/defaults/default.root.layout.js') + const defaultLayoutPath = join(__dirname, 'lib/defaults/default.root.layout.ts') const defaultGlobalStylePath = join(__dirname, 'lib/defaults/default.style.css') const defaultGlobalClientPath = join(__dirname, 'lib/defaults/default.client.js') + const layoutSource = await readFile(defaultLayoutPath, 'utf8') + const layout = language === 'ts' + ? layoutSource.replace("from '#types'", "from '@domstack/static/types.js'") + : stripTypeScriptTypes(layoutSource) + await mkdir(join(src, 'layouts'), { recursive: true }) await Promise.all([ - copyFile(defaultLayoutPath, join(src, targetLayoutPath)), + writeFile(join(src, targetLayoutPath), layout), copyFile(defaultGlobalStylePath, join(src, targetGlobalStylePath)), copyFile(defaultGlobalClientPath, join(src, targetGlobalClientPath)), ]) diff --git a/docs/cli/README.md b/docs/cli/README.md index 6d785e85..983e2032 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -28,6 +28,8 @@ Usage: domstack [options] --noEsbuildMeta skip writing the esbuild metafile to disk --domstackManifest write the domstack manifest to disk --eject, -e eject the DOMStack default layout, style and client into the src flag directory + --language language for --eject: ts or js (default: js) + --yes skip confirmation for --eject --watch, -w build, watch and serve the site build --watch-only watch and build the src folder without serving --verbose show debug logs, including the build tree and individual copy operations @@ -66,13 +68,29 @@ When you run `domstack --eject`, it will: 2. Create a default global CSS file at `globals/global.css` 3. - Create a default client-side JavaScript file at `globals/global.client.js` + Create a default client-side JavaScript file at `globals/global.client.js` (or `.mjs` depending on your package.json type) 4. Add the necessary dependencies to your package.json: - mine.css - fragtml - highlight.js +Use `domstack --eject --language ts` to write `layouts/root.layout.ts` and `globals/global.client.ts` instead. +For packages without `"type": "module"`, the TypeScript layout uses `.mts` so Node loads it as ESM without changing your package type. +The CSS and added dependencies are the same for both languages. +JavaScript remains the default (`--language js`), with `.js` files in module packages and `.mjs` files otherwise. +Only `ts` and `js` are accepted language values. + +DOMStack maintains one canonical TypeScript root layout and runs it using Node's native type stripping. +JavaScript eject output is derived from that source using `node:module`'s `stripTypeScriptTypes`, not a separate template. +The TypeScript output uses the public type-only `@domstack/static/types.js` entry instead of DOMStack's private `#types` alias. +Keep `@domstack/static` installed for those types; no runtime type import or separate TypeScript compilation step is needed. +The client is currently comment-only, but receives a `.ts` extension when TypeScript is selected. + +For automation, run `domstack --eject --language ts --yes --src src` to skip the confirmation prompt. +Without `--yes`, eject asks for confirmation before writing files or updating dependencies. +Eject overwrites its target files, so review or back up existing customizations before proceeding. + It is recommended to eject early in your project so that you can customize the root layout as you see fit, and decouple yourself from potential unwanted changes in the default layout as new versions of DOMStack are released. [domstack-sync]: https://www.npmjs.com/package/@domstack/sync diff --git a/docs/layouts/README.md b/docs/layouts/README.md index 7f887c52..d027a827 100644 --- a/docs/layouts/README.md +++ b/docs/layouts/README.md @@ -202,7 +202,9 @@ const defaultRootLayout: LayoutFunction} WorkerFiles @@ -28,7 +29,7 @@ import { resolveLayoutChain } from './resolve-layout-chain.js' * @returns {Promise<{ render: InternalLayoutFunction, vars: Partial, parentLayout: string | undefined }>} The resolved layout module exports. */ export async function resolveLayout (layoutPath) { - const { default: layout, vars, parentLayout } = await import(layoutPath) + const { default: layout, vars, parentLayout } = await loadLayout(layoutPath) if (typeof layout !== 'function') throw new TypeError(`Layout "${layoutPath}" must export a default render function`) if (parentLayout !== undefined && (typeof parentLayout !== 'string' || !parentLayout.trim())) { throw new TypeError(`Layout "${layoutPath}" parentLayout must be a non-empty string`) diff --git a/lib/defaults/default.root.layout.test.js b/lib/defaults/default.root.layout.test.js index 1ef4df97..6d4b5fdd 100644 --- a/lib/defaults/default.root.layout.test.js +++ b/lib/defaults/default.root.layout.test.js @@ -2,7 +2,7 @@ import assert from 'node:assert/strict' import { test } from 'node:test' import { load } from 'cheerio' import { html, raw, render } from 'fragtml' -import defaultRootLayout from './default.root.layout.js' +import defaultRootLayout from './default.root.layout.ts' test('string and HtmlResult children preserve whitespace through the root layout', async () => { const code = 'first line\n indented line\n\n\tlast line\n' diff --git a/lib/defaults/default.root.layout.js b/lib/defaults/default.root.layout.ts similarity index 75% rename from lib/defaults/default.root.layout.js rename to lib/defaults/default.root.layout.ts index 10646f12..2c7145ff 100644 --- a/lib/defaults/default.root.layout.js +++ b/lib/defaults/default.root.layout.ts @@ -1,23 +1,13 @@ -/** - * @import { LayoutFunction } from '#types' - * @import { HtmlResult } from 'fragtml/types.js' - */ +import type { LayoutFunctionParams } from '#types' +import type { HtmlResult } from 'fragtml/types.js' import { html, raw, render } from 'fragtml' -/** - * @typedef {{ - * title: string, - * siteName: string, - * defaultStyle: boolean, - * basePath: string - * }} DefaultRootLayoutVars - */ - -/** - * Build all of the bundles using esbuild. - * - * @type {LayoutFunction} - */ +export type DefaultRootLayoutVars = { + title: string + siteName: string + defaultStyle: boolean + basePath: string +} export default function defaultRootLayout ({ vars: { title, @@ -30,7 +20,7 @@ export default function defaultRootLayout ({ children, /* pages */ /* page */ -}) { +}: LayoutFunctionParams): string { return render(html` diff --git a/lib/defaults/load-layout.js b/lib/defaults/load-layout.js new file mode 100644 index 00000000..fd84f467 --- /dev/null +++ b/lib/defaults/load-layout.js @@ -0,0 +1,31 @@ +import { readFileSync } from 'node:fs' +import { registerHooks, stripTypeScriptTypes } from 'node:module' +import { pathToFileURL } from 'node:url' + +const defaultLayoutURL = new URL('./default.root.layout.ts', import.meta.url) + +/** + * Node will not automatically strip TypeScript inside node_modules. + * Explicitly strip only our canonical layout, preserving its URL for imports + * and diagnostics and leaving user modules to Node's normal loader. + * @param {string} layoutPath + */ +export async function loadLayout (layoutPath) { + if (pathToFileURL(layoutPath).href !== defaultLayoutURL.href) return import(layoutPath) + + const hooks = registerHooks({ + load (url, context, nextLoad) { + if (url !== defaultLayoutURL.href) return nextLoad(url, context) + return { + format: 'module', + source: stripTypeScriptTypes(readFileSync(defaultLayoutURL, 'utf8'), { sourceUrl: url }), + shortCircuit: true, + } + }, + }) + try { + return await import(defaultLayoutURL.href) + } finally { + hooks.deregister() + } +} diff --git a/lib/identify-pages.js b/lib/identify-pages.js index 4f2ee9ff..6ea44749 100644 --- a/lib/identify-pages.js +++ b/lib/identify-pages.js @@ -485,7 +485,7 @@ export async function identifyPages (src, opts = {}) { }) defaultLayout = true - const defaultLayoutBasename = 'default.root.layout.js' + const defaultLayoutBasename = 'default.root.layout.ts' const defaultLayoutFilepath = resolve(__dirname, `./defaults/${defaultLayoutBasename}`) const defaultLayoutRelpath = relative(src, defaultLayoutFilepath) diff --git a/package.json b/package.json index 55adbeae..511270e0 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "lib/**/*.d.ts.map", "lib/**/*.js", "lib/**/*.json", + "lib/defaults/default.root.layout.ts", "!lib/**/*.test.js", "!lib/**/fixtures/**", "page.vars.js", diff --git a/scripts/test-packed-types.js b/scripts/test-packed-types.js index b157dbc6..872cd460 100644 --- a/scripts/test-packed-types.js +++ b/scripts/test-packed-types.js @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict' import { execFile, spawn } from 'node:child_process' import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' @@ -21,7 +22,11 @@ try { ['pack', '--json', '--ignore-scripts', '--pack-destination', temporaryPath], { cwd: projectPath, encoding: 'utf8' } ) - const [{ filename }] = JSON.parse(stdout) + const [{ filename, files }] = JSON.parse(stdout) + const packedPaths = files.map(/** @param {{ path: string }} file */ file => file.path) + assert.ok(packedPaths.includes('lib/defaults/default.root.layout.ts')) + assert.ok(packedPaths.includes('lib/defaults/default.root.layout.d.ts')) + assert.ok(!packedPaths.includes('lib/defaults/default.root.layout.js'), 'no duplicate JavaScript layout is packaged') const tarballPath = path.join(temporaryPath, filename) await mkdir(consumerPath) @@ -101,6 +106,27 @@ void ({} as Results) ['install', '--ignore-scripts', '--no-audit', '--no-fund', '--no-package-lock'], consumerPath ) + const packedBin = path.join(consumerPath, 'node_modules', '@domstack/static', 'bin.js') + for (const language of ['ts', 'js']) { + const src = `src-${language}` + await mkdir(path.join(consumerPath, src)) + await writeFile(path.join(consumerPath, src, 'page.html'), '

Packed defaults

') + await run(process.execPath, [packedBin, '--eject', '--language', language, '--yes', '--src', src], consumerPath) + await run(process.execPath, [packedBin, '--src', src, '--dest', `public-${language}`], consumerPath) + assert.match(await readFile(path.join(consumerPath, `public-${language}`, 'index.html'), 'utf8'), /

Packed defaults<\/h1>/) + } + await mkdir(path.join(consumerPath, 'src-default')) + await writeFile(path.join(consumerPath, 'src-default', 'page.html'), '

Canonical default

') + await run(process.execPath, [packedBin, '--src', 'src-default', '--dest', 'public-default'], consumerPath) + assert.match(await readFile(path.join(consumerPath, 'public-default', 'index.html'), 'utf8'), /

Canonical default<\/h1>/) + await writeFile(path.join(consumerPath, 'tsconfig-eject.json'), `${JSON.stringify({ + extends: './tsconfig.json', + // fragtml currently ships a declaration with an unresolved FragmentBoundary. + // Still check our ejected source and its public imports, not dependency internals. + compilerOptions: { skipLibCheck: true }, + include: ['src-ts/**/*.ts'], + }, null, 2)}\n`) + // Node 26 exercises the missing alias; 24 and 22 guard against duplicate // declarations on supported older releases. Check each entry in isolation. for (const nodeVersion of [26, 24, 22]) { @@ -111,7 +137,7 @@ void ({} as Results) consumerPath ) } - for (const config of ['tsconfig.json', 'tsconfig-types.json']) { + for (const config of ['tsconfig.json', 'tsconfig-types.json', 'tsconfig-eject.json']) { console.log(`Checking TypeScript ${devDependencies.typescript}, @types/node ${nodeVersion}, ${config}`) await run( process.execPath, diff --git a/test-cases/cli-errors/eject.test.js b/test-cases/cli-errors/eject.test.js new file mode 100644 index 00000000..d4f0f346 --- /dev/null +++ b/test-cases/cli-errors/eject.test.js @@ -0,0 +1,74 @@ +import assert from 'node:assert/strict' +import { execFile, spawnSync } from 'node:child_process' +import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { stripTypeScriptTypes } from 'node:module' +import { promisify } from 'node:util' +import { test } from 'node:test' + +const exec = promisify(execFile) +const project = resolve(import.meta.dirname, '../..') +const bin = join(project, 'bin.js') + +for (const type of ['module', 'commonjs']) { + for (const language of [undefined, 'js', 'ts']) { + test(`eject ${language ?? 'default'} into ${type} package and build`, async t => { + const cwd = await mkdtemp(join(tmpdir(), 'domstack-eject-')) + t.after(() => rm(cwd, { recursive: true, force: true })) + await writeFile(join(cwd, 'package.json'), JSON.stringify({ type, dependencies: { retained: '1.0.0' } })) + await mkdir(join(cwd, 'src')) + await writeFile(join(cwd, 'src/page.html'), '

Ejected site

') + await symlink(join(project, 'node_modules'), join(cwd, 'node_modules'), 'dir') + const args = [bin, '--eject', '--yes', ...(language ? ['--language', language] : [])] + const { stdout } = await exec(process.execPath, args, { cwd, timeout: 30000 }) + assert.match(stdout, /Done ejecting files!/) + assert.doesNotMatch(stdout, /Continue\?/) + const extension = language === 'ts' ? (type === 'module' ? 'ts' : 'mts') : type === 'module' ? 'js' : 'mjs' + const layout = await readFile(join(cwd, `src/layouts/root.layout.${extension}`), 'utf8') + const canonical = await readFile(join(project, 'lib/defaults/default.root.layout.ts'), 'utf8') + assert.equal(layout, language === 'ts' + ? canonical.replace("from '#types'", "from '@domstack/static/types.js'") + : stripTypeScriptTypes(canonical)) + assert.doesNotMatch(layout, /#types/) + assert.equal(await readFile(join(cwd, `src/globals/global.client.${language === 'ts' ? 'ts' : extension}`), 'utf8'), + await readFile(join(project, 'lib/defaults/default.client.js'), 'utf8')) + assert.equal(await readFile(join(cwd, 'src/globals/global.css'), 'utf8'), + await readFile(join(project, 'lib/defaults/default.style.css'), 'utf8')) + const pkg = JSON.parse(await readFile(join(cwd, 'package.json'), 'utf8')) + assert.equal(pkg.dependencies.retained, '1.0.0') + for (const dependency of ['mine.css', 'fragtml', 'highlight.js']) { + assert.ok(pkg.dependencies[dependency]) + } + await exec(process.execPath, [bin], { cwd, timeout: 30000 }) + assert.match(await readFile(join(cwd, 'public/index.html'), 'utf8'), /

Ejected site<\/h1>/) + }) + } +} + +test('eject still asks for confirmation and respects a declined prompt', async t => { + const cwd = await mkdtemp(join(tmpdir(), 'domstack-eject-prompt-')) + t.after(() => rm(cwd, { recursive: true, force: true })) + const pkg = '{"type":"module"}' + await writeFile(join(cwd, 'package.json'), pkg) + await mkdir(join(cwd, 'src')) + const result = spawnSync(process.execPath, [bin, '--eject'], { + cwd, input: 'n\n', encoding: 'utf8', timeout: 30000, + }) + assert.ifError(result.error) + assert.equal(result.status, 0) + assert.match(result.stdout, /Continue\?/) + assert.match(result.stdout, /No action taken/) + assert.equal(await readFile(join(cwd, 'package.json'), 'utf8'), pkg) + await assert.rejects(readFile(join(cwd, 'src/layouts/root.layout.js')), { code: 'ENOENT' }) +}) + +test('invalid eject language fails without changing the project', async t => { + const cwd = await mkdtemp(join(tmpdir(), 'domstack-eject-invalid-')) + t.after(() => rm(cwd, { recursive: true, force: true })) + const pkg = '{"type":"module"}' + await writeFile(join(cwd, 'package.json'), pkg) + await assert.rejects(exec(process.execPath, [bin, '--eject', '--yes', '--language', 'tsx'], { cwd, timeout: 30000 }), /--language must be ts or js/) + assert.equal(await readFile(join(cwd, 'package.json'), 'utf8'), pkg) + await assert.rejects(readFile(join(cwd, 'src/layouts/root.layout.js')), { code: 'ENOENT' }) +}) From 35e6aca670e77a3b359ab285cf8fbf61a3681ed6 Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sun, 13 Sep 2026 16:33:16 -0700 Subject: [PATCH 2/2] Build and version the JavaScript default layout before publishing --- CONTRIBUTING.md | 18 ++++- agents.md | 1 + bin.js | 5 +- docs/cli/README.md | 4 +- docs/layouts/README.md | 6 +- eslint.config.js | 1 + lib/build-pages/page-data.js | 4 +- lib/defaults/default.root.layout.js | 39 +++++++++++ lib/defaults/default.root.layout.test.js | 19 ++++- lib/defaults/load-layout.js | 31 --------- lib/identify-pages.js | 2 +- package.json | 7 +- scripts/build-defaults.js | 13 ++++ scripts/test-packed-types.js | 16 ++++- scripts/test-version-build.js | 89 ++++++++++++++++++++++++ test-cases/cli-errors/eject.test.js | 3 +- 16 files changed, 205 insertions(+), 53 deletions(-) create mode 100644 lib/defaults/default.root.layout.js delete mode 100644 lib/defaults/load-layout.js create mode 100644 scripts/build-defaults.js create mode 100644 scripts/test-version-build.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9e4b5fb..ae648d1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,14 @@ - Questions are welcome, however unless there is a official support contract established between the maintainers and the requester, support is not guaranteed. - Contributors reserve the right to walk away from this project at any moment with or without notice. +## Generated default layout + +Edit `lib/defaults/default.root.layout.ts`, then run `npm run build:defaults` to regenerate `lib/defaults/default.root.layout.js`. +The JavaScript is checked in so normal development and tests work immediately after checkout, without a declaration build or a runtime TypeScript loader. +Do not edit the generated JavaScript directly. +Both layouts are published, and eject copies the requested language (rewriting the TypeScript type import to the public package entry). +Declaration cleanup and `npm run clean` deliberately preserve the generated JavaScript. + ## Releasing Changelog, and releasing is automated with npm scripts and actions. To create a release: @@ -29,6 +37,12 @@ If for some reason that isn't working or a local release is preferred, follow th - Ensure a clean working git workspace. - Run `npm version {patch,minor,major}`. - - This wills update the version number and generate the changelog. + - This updates the version number, builds the default JavaScript and version-dependent manifest schema, and uses `releasearoni version --add` to stage both generated files alongside the changelog before npm creates the version commit and tag. - Run `npm publish`. - - This will push your local git branch and tags to the default remote, perform a [gh-release](https://ghub.io/gh-release), and create an npm publication + - `releasearoni` runs the full build before pushing the branch and tags and creating the GitHub release. + - The `prepack` hook cleans old declarations before regenerating the default JavaScript and declarations for both `npm pack` and `npm publish`, so tarballs include both layout languages and their types even after a full release build. + - Post-publish cleanup removes temporary declarations and site output, but preserves versioned JavaScript so it does not dirty the version commit. + +Generation belongs in `version`, not `preversion` (which runs before the version update) or `postversion` (which runs after the commit and tag). +The release workflow's pre-version reset/clean is safe because the initial generated JavaScript is tracked and the version hook rebuilds it before staging. +Run `npm run test:version-build` to verify generation, staging, tagging, and cleanup in a disposable repository without versioning this checkout. diff --git a/agents.md b/agents.md index 7249a1e4..604a0ef3 100644 --- a/agents.md +++ b/agents.md @@ -10,5 +10,6 @@ - Type builds are only needed during publish time or when debugging types. - After running a type build, clean up the generated build files and do not leave them sitting around. - Use the cleanup scripts in `package.json` for generated type build files. +- The generated `lib/defaults/default.root.layout.js` is versioned runtime code, not temporary declaration output; regenerate it with `npm run build:defaults` after editing its TypeScript source and never remove it during cleanup. - For formatting-only ESLint failures, use `npx eslint --fix` for a quick targeted fix before rerunning lint. - When handling PR review comments, validate that each comment is correct before making changes; maintainer comments are almost always valid, but review bot comments may be wrong, and after addressing a comment, always reply with what was done. diff --git a/bin.js b/bin.js index 556823ac..45e4be92 100755 --- a/bin.js +++ b/bin.js @@ -8,7 +8,6 @@ */ import { mkdir, readFile, writeFile } from 'node:fs/promises' -import { stripTypeScriptTypes } from 'node:module' import { basename, resolve, join, relative } from 'node:path' import { parseArgs } from 'node:util' import { printHelpText } from 'argsclopts' @@ -210,14 +209,14 @@ domstack eject actions: } } - const defaultLayoutPath = join(__dirname, 'lib/defaults/default.root.layout.ts') + const defaultLayoutPath = join(__dirname, `lib/defaults/default.root.layout.${language}`) const defaultGlobalStylePath = join(__dirname, 'lib/defaults/default.style.css') const defaultGlobalClientPath = join(__dirname, 'lib/defaults/default.client.js') const layoutSource = await readFile(defaultLayoutPath, 'utf8') const layout = language === 'ts' ? layoutSource.replace("from '#types'", "from '@domstack/static/types.js'") - : stripTypeScriptTypes(layoutSource) + : layoutSource await mkdir(join(src, 'layouts'), { recursive: true }) await Promise.all([ writeFile(join(src, targetLayoutPath), layout), diff --git a/docs/cli/README.md b/docs/cli/README.md index 983e2032..5c7311fe 100644 --- a/docs/cli/README.md +++ b/docs/cli/README.md @@ -81,8 +81,8 @@ The CSS and added dependencies are the same for both languages. JavaScript remains the default (`--language js`), with `.js` files in module packages and `.mjs` files otherwise. Only `ts` and `js` are accepted language values. -DOMStack maintains one canonical TypeScript root layout and runs it using Node's native type stripping. -JavaScript eject output is derived from that source using `node:module`'s `stripTypeScriptTypes`, not a separate template. +DOMStack maintains one canonical TypeScript root layout and generates its JavaScript counterpart at build and release time. +Both files are published; the runtime loads JavaScript directly without a custom loader, and eject copies the selected language rather than compiling it. The TypeScript output uses the public type-only `@domstack/static/types.js` entry instead of DOMStack's private `#types` alias. Keep `@domstack/static` installed for those types; no runtime type import or separate TypeScript compilation step is needed. The client is currently comment-only, but receives a `.ts` extension when TypeScript is selected. diff --git a/docs/layouts/README.md b/docs/layouts/README.md index d027a827..10875e51 100644 --- a/docs/layouts/README.md +++ b/docs/layouts/README.md @@ -202,8 +202,10 @@ const defaultRootLayout: LayoutFunction} WorkerFiles @@ -29,7 +29,7 @@ import { loadLayout } from '../defaults/load-layout.js' * @returns {Promise<{ render: InternalLayoutFunction, vars: Partial, parentLayout: string | undefined }>} The resolved layout module exports. */ export async function resolveLayout (layoutPath) { - const { default: layout, vars, parentLayout } = await loadLayout(layoutPath) + const { default: layout, vars, parentLayout } = await import(pathToFileURL(layoutPath).href) if (typeof layout !== 'function') throw new TypeError(`Layout "${layoutPath}" must export a default render function`) if (parentLayout !== undefined && (typeof parentLayout !== 'string' || !parentLayout.trim())) { throw new TypeError(`Layout "${layoutPath}" parentLayout must be a non-empty string`) diff --git a/lib/defaults/default.root.layout.js b/lib/defaults/default.root.layout.js new file mode 100644 index 00000000..a87badde --- /dev/null +++ b/lib/defaults/default.root.layout.js @@ -0,0 +1,39 @@ +// Generated from default.root.layout.ts by npm run build:defaults. Do not edit. +var __freeze = Object.freeze; +var __defProp = Object.defineProperty; +var __template = (cooked, raw2) => __freeze(__defProp(cooked, "raw", { value: __freeze(raw2 || cooked.slice()) })); +var _a; +import { html, raw, render } from "fragtml"; +function defaultRootLayout({ + vars: { + title, + siteName = "domstack", + basePath + /* defaultStyle = true Set this to false in global or page to disable the default style in the default layout */ + }, + scripts, + styles, + children + /* pages */ + /* page */ +}) { + return render(html` + + + + + ${title ? `${title}` : ""}${title && siteName ? " | " : ""}${siteName} + + + ${scripts ? scripts.map((script) => html(_a || (_a = __template(['