diff --git a/AGENTS.md b/AGENTS.md index 119be43..21d7922 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,10 +16,11 @@ This file provides guidance to AI coding agents when working with code in this r - `pnpm build` — `tsc` emits `.d.ts` types, then `copy:source` uses esbuild to minify/bundle `src/*.js` + `src/utils/*` into `dist/` as ESM. **`dist/` is what gets published; `src/` is shipped as the readable source.** - `pnpm size-limit` — enforces the per-file byte budgets declared in `package.json` `size-limit` (each `dist/` file has its own, e.g. `WebComponent.js` ≤ 2.05 KB, `html.js` ≤ 0.6 KB, each util ≤ 0.5–0.8 KB). Keep additions tiny; this gate is a core project value. `size-change-log.md` is the running record of how each change moved the base-class bundle and why; its latest row's min+brotli figure is the single source of truth for the base-class size, so `docs/src/content/docs/guides/library-size.md` must be updated in lockstep whenever a new row lands. - `pnpm test:types` — builds, then type-checks the TS type tests (`test/types/typed-props.test-d.ts`) and the `demo/examples/typed-props/` example. `this.props` is typed by passing the props shape as a class type argument: `class X extends WebComponent`. +- `pnpm test:template` — scaffolds `create/template/` into a temp directory with the real scaffolder, links this repo in as `web-component-base`, and type-checks it with the template's own `tsconfig.json` (`scripts/check-template.js`). It resolves through the root `exports` map, so it catches a template that can't resolve the package — the failure mode that leaves every `this.props` access an error while `vite build` still exits 0. - `pnpm docs` — run the Astro docs site (`docs/` workspace) locally. - `pnpm demo` — run the Vite examples showcase (`demo/` workspace) locally; `pnpm demo:build` builds it. - `pnpm -F storybook dev` — run the Storybook testing ground (`storybook/`) locally; `pnpm -F storybook build` builds it. Both run `cem analyze` first to regenerate `custom-elements.json`. The CEM config imports the plugin from the **published subpath** (`web-component-base/cem-plugin` → `dist/`), so run `pnpm build` at the root first. -- `pnpm test:e2e` — run the browser e2e specs (`test/e2e/`) via Vitest browser mode (Playwright). Defaults to Chromium; `pnpm test:e2e:firefox` / `:webkit` / `:all` target the other engines (the `E2E_BROWSERS` env var, comma-separated, selects instances). `pnpm test:all` runs unit + types + e2e across all engines. +- `pnpm test:e2e` — run the browser e2e specs (`test/e2e/`) via Vitest browser mode (Playwright). Defaults to Chromium; `pnpm test:e2e:firefox` / `:webkit` / `:all` target the other engines (the `E2E_BROWSERS` env var, comma-separated, selects instances). `pnpm test:all` runs unit + types + template + e2e across all engines. - `pnpm create wcb` (the `create/` workspace package) scaffolds a new component project from `create/template/`. `create/` is a separate publishable package, not part of the `web-component-base` bundle. pnpm is mandatory (a `preinstall` `only-allow pnpm` guard enforces it). This is a pnpm workspace with three sub-packages: `docs/` (Astro docs site), `demo/` (Vite examples showcase, which consumes the root `web-component-base` package as a `workspace:*` dependency), and `storybook/` (Storybook testing ground — it writes no components of its own, it builds stories over the `demo/examples/` components so the CEM plugin is exercised against real ones). The runnable example sources live in `demo/examples/` and import the package by name (`web-component-base`), not via relative `src/` paths. @@ -56,7 +57,7 @@ Any change to observable behavior ships as one unit — code alone is an incompl 3. **Documentation** — the guide under `docs/src/content/docs/guides/` that doubles as the behavioral spec. For a breaking change, also update the `README.md` banner with the migration consumers have to perform. 4. **Size budget** — `pnpm size-limit` stays green. If an addition genuinely needs more headroom, raise the budget in `package.json` deliberately and say so in the change description; never let it drift silently. Whenever the change moves the base-class bundle, record it as a new row in `size-change-log.md` **and** update the headline size in `docs/src/content/docs/guides/library-size.md` to match that row's new min+brotli figure — the two must never disagree. -Verify with `pnpm test:all` (unit + types + e2e across all engines) before calling the change done. +Verify with `pnpm test:all` (unit + types + template + e2e across all engines) before calling the change done. ## Testing notes diff --git a/create/create.test.mjs b/create/create.test.mjs index 5a3739f..a991298 100644 --- a/create/create.test.mjs +++ b/create/create.test.mjs @@ -45,6 +45,7 @@ describe('create-wcb', () => { 'src/my-button.ts', 'custom-elements-manifest.config.mjs', 'vite-lib.config.ts', + 'tsconfig.json', 'pnpm-workspace.yaml', 'README.md', ]) diff --git a/create/template/tsconfig.json b/create/template/tsconfig.json new file mode 100644 index 0000000..1318a9e --- /dev/null +++ b/create/template/tsconfig.json @@ -0,0 +1,21 @@ +// TypeScript config for the component source. `npm run build:lib` runs +// vite-plugin-dts against this to emit `.d.ts` files next to the bundles. +// +// `moduleResolution: "bundler"` is the load-bearing line. Without it tsc falls +// back to classic resolution, never looks in node_modules, and the +// `web-component-base` import fails — which in turn makes every `this.props` +// access an error, because the base class it resolves to is an error type. +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "skipLibCheck": true, + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/package.json b/package.json index 7de59e1..c5edb96 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "test:e2e:webkit": "E2E_BROWSERS=webkit vitest --run --config vitest.e2e.config.mjs", "test:e2e:all": "vitest --run --config vitest.e2e.config.mjs", "test:types": "pnpm run build && tsc -p test/types/tsconfig.json && tsc -p demo/examples/typed-props/tsconfig.json", - "test:all": "pnpm test && pnpm test:types && pnpm test:e2e:all", + "test:template": "pnpm run build && node ./scripts/check-template.js", + "test:all": "pnpm test && pnpm test:types && pnpm test:template && pnpm test:e2e:all", "demo": "pnpm -F demo dev", "demo:build": "pnpm -F demo build", "docs": "pnpm -F docs start", diff --git a/scripts/check-template.js b/scripts/check-template.js new file mode 100644 index 0000000..0731bf0 --- /dev/null +++ b/scripts/check-template.js @@ -0,0 +1,75 @@ +/** + * @license MIT + * + * Type-checks the `create/` scaffold the way a user gets it: runs the real + * scaffolder into a temp directory, links this repo in as + * `web-component-base`, and runs `tsc` with the template's own + * `tsconfig.json`. + * + * The link resolves through the root `package.json` `exports` map, so this + * exercises the same module resolution a scaffolded project does — which is + * what catches a template that ships no `tsconfig.json`, or one whose + * `moduleResolution` can't read an `exports` map. Both leave the + * `web-component-base` import unresolvable and every `this.props` access an + * error, while `vite build` still exits 0. + * + * Run via `pnpm test:template` (part of `pnpm test:all`). Needs `dist/` built. + */ + +import { execFileSync } from 'node:child_process' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +const repoRoot = fileURLToPath(new URL('..', import.meta.url)) +const scaffolder = path.join(repoRoot, 'create', 'index.js') +const tsc = path.join(repoRoot, 'node_modules', '.bin', 'tsc') +const PROJECT = 'template-check' + +if (!fs.existsSync(path.join(repoRoot, 'dist', 'index.d.ts'))) { + console.error('check-template: dist/index.d.ts missing — run `pnpm build`') + process.exit(1) +} + +const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wcb-template-')) +const root = path.join(workDir, PROJECT) + +try { + execFileSync(process.execPath, [scaffolder, PROJECT], { + cwd: workDir, + stdio: 'pipe', + }) + + // Stand in for `npm install` of the peer dependency: the scaffold resolves + // `web-component-base` to this repo, exports map and all. + fs.mkdirSync(path.join(root, 'node_modules'), { recursive: true }) + fs.symlinkSync( + repoRoot, + path.join(root, 'node_modules', 'web-component-base'), + 'dir' + ) + + // The template emits declarations during `build:lib`; here we only want the + // diagnostics, and `emitDeclarationOnly` may not be combined with `noEmit`. + execFileSync( + tsc, + ['-p', 'tsconfig.json', '--emitDeclarationOnly', 'false', '--noEmit'], + { + cwd: root, + stdio: 'inherit', + } + ) + + console.log('check-template: scaffold type-checks clean') +} catch { + console.error( + `\ncheck-template: the scaffolded template does not type-check.\n` + + `A user running \`npm create wcb@latest\` sees these errors on their\n` + + `first \`npm run build:lib\`. Check create/template/tsconfig.json.` + ) + process.exit(1) +} finally { + fs.rmSync(workDir, { recursive: true, force: true }) +}