@webjsdev/cli 0.10.10 → 0.10.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,334 @@
1
+ # TypeScript without a build step + full-stack type safety
2
+
3
+ Files ending in `.ts` / `.mts` are supported everywhere `.js` / `.mjs`
4
+ are. Same routing conventions, same server-action behaviour. No `tsc`
5
+ run is part of the user-visible workflow, no separate build step:
6
+
7
+ - **Editor** (VS Code) runs the TypeScript language server continuously. Red-squiggle on wrong types, including non-erasable syntax (see below).
8
+ - **CI** (optional) runs `tsc --noEmit` against `tsconfig.json`. Type-check only. Also catches non-erasable syntax via `erasableSyntaxOnly`.
9
+ - **Dev + prod server** (runtime, both directions): Node 24+'s built-in TypeScript type-stripping handles server-side `.ts` imports automatically (`process.features.typescript === 'strip'`). Browser-bound `.ts` requests go through `module.stripTypeScriptTypes` on the dev server, which performs whitespace replacement: every (line, column) in the source maps to the same position in the stripped output, so no sourcemap needs to be shipped and stack traces are byte-exact. The transform is cached by mtime (~microseconds per cache hit). Implementation backing: Node ships the [`amaro`](https://github.com/nodejs/amaro) package internally, which wraps SWC's WASM TypeScript transform in a position-preserving strip-only mode. If the framework ever needs to run on a non-Node runtime (Bun, Deno) we will install `amaro` directly or an equivalent position-preserving stripper (Sucrase preserves lines but not columns; SWC's strip mode also works).
10
+
11
+ ## TypeScript feature support: erasable only
12
+
13
+ The framework uses Node 24+'s built-in `module.stripTypeScriptTypes`,
14
+ which only supports **erasable TypeScript**: type annotations,
15
+ `interface`, `type`, `declare`, generics, `import type`, `as` casts,
16
+ and `satisfies`. Non-erasable syntax is rejected.
17
+
18
+ Use the **erasable equivalents** instead:
19
+
20
+ ```ts
21
+ // Not allowed (rejected at compile + runtime)
22
+ enum Color { Red, Green, Blue }
23
+ class Foo { constructor(public x: number) {} }
24
+ namespace Util { export const helper = ...; }
25
+ import = require('something');
26
+ @legacyDecorator class C {}
27
+
28
+ // Allowed (canonical erasable forms)
29
+ const Color = { Red: 'Red', Green: 'Green', Blue: 'Blue' } as const;
30
+ type Color = typeof Color[keyof typeof Color];
31
+
32
+ class Foo {
33
+ x: number;
34
+ constructor(x: number) { this.x = x; }
35
+ }
36
+
37
+ const Util = { helper: ... };
38
+
39
+ import { thing } from './thing.ts';
40
+ ```
41
+
42
+ Enforce this at edit time by setting `erasableSyntaxOnly: true` in
43
+ `tsconfig.json`. The TypeScript compiler then flags any non-erasable
44
+ syntax as a red squiggle in the editor and `tsc --noEmit` error in CI.
45
+
46
+ The `erasable-typescript-only` convention check verifies the flag is
47
+ set. Run `webjs check` to confirm.
48
+
49
+ ### Fallback for third-party `.ts` dependencies
50
+
51
+ If a third-party package ships `.ts` source using non-erasable
52
+ syntax (rare; most npm packages publish compiled `.js`), the dev
53
+ server fails at strip time and returns a 500 naming the file and
54
+ pointing at the `no-non-erasable-typescript` lint rule. webjs is
55
+ buildless end-to-end and has no bundler fallback. Your own code
56
+ never hits this as long as `erasableSyntaxOnly` is set.
57
+
58
+ If you manually turn `erasableSyntaxOnly` off and write non-erasable
59
+ syntax in your own code, the dev server fails the same way. The
60
+ `erasable-typescript-only` convention check warns when the flag is
61
+ off so you catch the configuration drift before runtime.
62
+
63
+ ## Import convention
64
+
65
+ Use explicit `.ts` extensions in imports. Node 24+'s built-in
66
+ type-stripping and the dev server's HTTP handler both key on the
67
+ file URL ending in `.ts` / `.mts`. For mixed codebases, `.js` imports
68
+ that point at a `.ts` sibling also resolve in the dev server. Still
69
+ prefer explicit `.ts`.
70
+
71
+ ```ts
72
+ // modules/posts/queries/list-posts.server.ts
73
+ import { prisma } from '../../../lib/prisma.js'; // JS file unchanged
74
+ import { formatPost } from '../utils/slugify.ts'; // TS file
75
+ ```
76
+
77
+ ## Minimum viable `tsconfig.json`
78
+
79
+ ```json
80
+ {
81
+ "compilerOptions": {
82
+ "target": "ES2022",
83
+ "module": "NodeNext",
84
+ "moduleResolution": "NodeNext",
85
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
86
+ "strict": true,
87
+ "noEmit": true,
88
+ "checkJs": true,
89
+ "allowJs": true,
90
+ "allowImportingTsExtensions": true,
91
+ "skipLibCheck": true,
92
+ "erasableSyntaxOnly": true
93
+ }
94
+ }
95
+ ```
96
+
97
+ The `erasableSyntaxOnly: true` line is the non-negotiable one. It
98
+ aligns the TypeScript compiler's accepted syntax with what Node's
99
+ strip-types accepts, so violations surface as editor diagnostics
100
+ instead of a runtime 500.
101
+
102
+ ## Full-stack type safety
103
+
104
+ ### Server actions: type-safe automatically
105
+
106
+ Calling a server action from a client component resolves at type-check
107
+ time to the action's real source file. The dev server's runtime stub
108
+ replacement is invisible to the type checker.
109
+
110
+ ```ts
111
+ // modules/posts/actions/create-post.server.ts
112
+ export async function createPost(
113
+ input: { title: string; body: string },
114
+ ): Promise<ActionResult<PostFormatted>> { /* … */ }
115
+
116
+ // modules/posts/components/new-post.ts
117
+ import { createPost } from '../actions/create-post.server.ts';
118
+ const r = await createPost({ title, body });
119
+ // ^ Promise<ActionResult<PostFormatted>>
120
+ if (r.success) r.data.title; // ← PostFormatted.title: string
121
+ ```
122
+
123
+ **Runtime matches types** because the RPC wire uses webjs's built-in
124
+ ESM serializer: `Date` → `Date`, `Map` → `Map`, `BigInt` → `BigInt`.
125
+ Supported: `Date`, `Map`, `Set`, `BigInt`, `Error`, `undefined`,
126
+ `NaN`/`Infinity`/`-0`, `TypedArray`, `ArrayBuffer`, `DataView`, `Blob`,
127
+ `File`, `FormData`, `Symbol.for(...)`, reference cycles. Class
128
+ instances come through as plain objects, with prototypes lost and methods
129
+ gone (matches React Server Actions).
130
+
131
+ ### API routes: opt in via content negotiation
132
+
133
+ `route.ts` handlers use standard JSON by default so external consumers
134
+ keep working. Opt into rich types for your own UI code:
135
+
136
+ ```ts
137
+ // app/api/posts/route.ts: server
138
+ import { json } from '@webjsdev/server';
139
+ import { listPosts } from '../../../modules/posts/queries/list-posts.server.ts';
140
+
141
+ export async function GET() {
142
+ return json(await listPosts()); // content-negotiates automatically
143
+ }
144
+ ```
145
+
146
+ ```ts
147
+ // caller: client
148
+ import { richFetch } from '@webjsdev/core';
149
+ const posts = await richFetch<Post[]>('/api/posts');
150
+ // posts[0].createdAt is a Date here.
151
+ ```
152
+
153
+ The `json()` helper reads the in-flight Request via AsyncLocalStorage:
154
+ - `Accept: application/vnd.webjs+json` → encoded with the webjs serializer, served with `Content-Type: application/vnd.webjs+json` and `Vary: Accept`.
155
+ - Otherwise → plain JSON.
156
+
157
+ Request bodies parse with `readBody(req)` from `@webjsdev/server`.
158
+
159
+ ### Page metadata: the `Metadata` type
160
+
161
+ A page or layout exports `metadata` (static) or `generateMetadata(ctx)`
162
+ (request-scoped). Annotate the return with the exported `Metadata` type so
163
+ a misspelled field or a wrong-typed value is a compile-time error, the
164
+ same ergonomics as Next.js's `import type { Metadata } from 'next'`.
165
+
166
+ ```ts
167
+ import type { Metadata, MetadataContext } from '@webjsdev/core';
168
+
169
+ export const metadata: Metadata = {
170
+ title: 'Blog',
171
+ description: 'Latest posts',
172
+ openGraph: { type: 'website', image: '/og.png' },
173
+ twitter: { card: 'summary_large_image' },
174
+ };
175
+
176
+ export async function generateMetadata(ctx: MetadataContext): Promise<Metadata> {
177
+ return { title: `Post: ${ctx.params.slug}`, metadataBase: new URL(ctx.url).origin };
178
+ }
179
+ ```
180
+
181
+ `Metadata` covers every field the SSR pipeline reads (see
182
+ `agent-docs/metadata.md`); each field is optional, and string-or-object
183
+ fields (`title`, `viewport`, `robots`, `appleWebApp`, `icons`) are unions.
184
+ It is a pure type in `packages/core/src/metadata.d.ts`, so it is erased at
185
+ runtime with zero build cost. `MetadataContext` types the
186
+ `generateMetadata` argument (`{ params, searchParams, url, actionData }`,
187
+ where `actionData` is set only on a failed-page-action re-render).
188
+
189
+ ### Typed page / layout / route-handler props (`PageProps`, `LayoutProps`, `RouteHandlerContext`)
190
+
191
+ A page default-export receives `{ params, searchParams, url, actionData }`; a
192
+ layout receives the same plus `children`; a `route.{js,ts}` handler receives
193
+ `(request, { params })`. Type each with the exported helpers so a typo in a
194
+ param name or a wrong-typed field is a compile-time error.
195
+
196
+ ```ts
197
+ import type { PageProps, LayoutProps, RouteHandlerContext } from '@webjsdev/core';
198
+
199
+ // A static route: `params` is `Record<string, string>`.
200
+ export default function About({ searchParams }: PageProps) { /* ... */ }
201
+
202
+ // A dynamic route: pass the route literal to narrow `params`.
203
+ export default function Post({ params }: PageProps<'/blog/[slug]'>) {
204
+ const slug = params.slug; // typed `string`
205
+ /* ... */
206
+ }
207
+
208
+ // A layout adds `children: TemplateResult`.
209
+ export default function RootLayout({ children }: LayoutProps) { /* ... */ }
210
+
211
+ // A route handler's 2nd arg.
212
+ export async function GET(req: Request, ctx: RouteHandlerContext) {
213
+ return Response.json({ id: ctx.params.id });
214
+ }
215
+ ```
216
+
217
+ `PageProps<R>` / `LayoutProps<R>` / `RouteHandlerContext<R>` take an optional
218
+ route literal `R`. With no `R` (or in an app that has not generated route
219
+ types), `params` is `Record<string, string>`, the runtime default. With `R`
220
+ set to a generated dynamic route, `params` narrows to its exact shape
221
+ (`{ slug: string }`, `{ rest: string[] }`, `{ slug?: string[] }`). The shapes
222
+ mirror what `packages/server/src/ssr.js` and `packages/server/src/api.js`
223
+ actually pass, NOT Next.js's superset. Pure types in
224
+ `packages/core/src/routes.d.ts`, erased at runtime with zero build cost.
225
+
226
+ ### The generated route union (`webjs types`) types `navigate()` and catches bad hrefs
227
+
228
+ Run `webjs types` to generate `.webjs/routes.d.ts`, an opt-in overlay that
229
+ augments `@webjsdev/core` with one key per route in `app/`. It narrows two
230
+ things at tsserver time:
231
+
232
+ - The `Route` href type: `navigate('/blog/anything')` is accepted,
233
+ `navigate('/nonexistent')` is a type error. (Until you generate the types,
234
+ `Route` is `string`, so `navigate()` is unconstrained, non-breaking for
235
+ JSDoc apps and un-generated apps alike.)
236
+ - Per-route `params`: `PageProps<'/blog/[slug]'>['params']` becomes
237
+ `{ slug: string }`, derived from the generated `RouteParamMap`.
238
+
239
+ ```sh
240
+ webjs types # writes .webjs/routes.d.ts (count of routes printed)
241
+ ```
242
+
243
+ `webjs dev` also emits it automatically at startup and re-emits after each
244
+ route rebuild, so an editor always has fresh route types. The file is
245
+ gitignored (regenerated per machine, like Next's `.next/types`); the scaffold
246
+ `tsconfig.json` lists `.webjs/routes.d.ts` in `include` so tsserver picks it
247
+ up. To opt in for an existing app, run `webjs types` once and ensure your
248
+ `tsconfig.json` `include` lists `.webjs/routes.d.ts`.
249
+
250
+ This is webjs's no-build equivalent of Next 15's `typedRoutes`, achieved via
251
+ interface declaration-merging (`declare module '@webjsdev/core'`) rather than a
252
+ bundler. The mechanism is `generateRouteTypes(appDir)` in
253
+ `packages/server/src/route-types.js`, which reuses the one route enumerator
254
+ (`buildRouteTable`). Output is deterministic (sorted keys), so re-running
255
+ yields a byte-identical file.
256
+
257
+ ### The `webjs` package.json config block: `WebjsConfig` + JSON Schema
258
+
259
+ The `webjs` object in `package.json` (the `elide` / `headers` / `redirects` /
260
+ `trailingSlash` / `csp` knobs plus the ingress body-size and timeout caps) has
261
+ two typed references, so a typo'd key is diagnosed instead of silently dropped:
262
+
263
+ - **A JSON Schema**, `packages/server/webjs-config.schema.json` (shipped in the
264
+ `@webjsdev/server` package). The scaffold's `.vscode/settings.json` associates
265
+ it with the `webjs` property of `package.json`, so VS Code flags an unknown
266
+ key natively while you edit the JSON. `additionalProperties: false` on the
267
+ block is what turns a typo into an editor warning.
268
+ - **The `WebjsConfig` type**, exported from `@webjsdev/core`, a typed reference
269
+ for an agent or human authoring the block (with `WebjsHeaderRule`,
270
+ `WebjsRedirectRule`, `WebjsCspConfig`, `WebjsTrailingSlash` for the nested
271
+ shapes).
272
+
273
+ ```ts
274
+ import type { WebjsConfig } from '@webjsdev/core';
275
+
276
+ const config: WebjsConfig = {
277
+ trailingSlash: 'never',
278
+ csp: true,
279
+ redirects: [{ source: '/old', destination: '/new' }],
280
+ };
281
+ ```
282
+
283
+ The schema and the type mirror what the server readers actually consume
284
+ (`readElideEnabled`, `compileHeaderRules`, `compileRedirectRules` /
285
+ `readTrailingSlashPolicy`, `readCspConfig`, `readBodyLimits` /
286
+ `computeServerTimeouts`). Adding a `webjs.*` key means updating the schema, the
287
+ type, AND the reader in lockstep, the one procedure documented in
288
+ `packages/server/AGENTS.md`. A drift test
289
+ (`packages/server/test/config/webjs-config-schema.test.js`) fails if the schema
290
+ and the reader key set diverge.
291
+
292
+ ### Both runtime packages ship a type overlay
293
+
294
+ Both `@webjsdev/core` AND `@webjsdev/server` ship a hand-authored `.d.ts`
295
+ overlay plus a `types` export condition, so a `strict` + `nodenext` app
296
+ resolves real types for either import with no TS7016 ("could not find a
297
+ declaration file") error. The server overlay (`packages/server/index.d.ts`,
298
+ with `src/check.d.ts` and `src/testing.d.ts` for the `./check` / `./testing`
299
+ subpaths) types the full public surface (`createRequestHandler`, `startServer`,
300
+ `cors`, `cache`, `createAuth`, `rateLimit`, `sitemap`, `Session`, `json`,
301
+ `readBody`, the `revalidate*` family, the context helpers, the cache stores, the
302
+ auth providers, the test harness, and the convention validator), reusing the
303
+ core prop / metadata types rather than redefining them. The runtime stays plain
304
+ `.js` + JSDoc; the overlay is types-only with zero runtime cost. A drift test
305
+ keeps `index.d.ts` in lockstep with `index.js`'s runtime exports.
306
+
307
+ ### TypeScript is not required
308
+
309
+ JS + JSDoc gets the same call-site type safety. The TypeScript language
310
+ server reads `@typedef` / `@param` / `@returns` identically to `.ts`
311
+ syntax. Add `"checkJs": true` to enforce types in editor + CI.
312
+
313
+ ## Editor plugin: `@webjsdev/ts-plugin`
314
+
315
+ **Editor-only. Not required for the framework to run.** The runtime
316
+ has no dependency on it.
317
+
318
+ A single plugin. As of `@webjsdev/ts-plugin@0.4.0`, `ts-lit-plugin`
319
+ is bundled internally, so list one entry:
320
+
321
+ ```jsonc
322
+ "plugins": [
323
+ { "name": "@webjsdev/ts-plugin" }
324
+ ]
325
+ ```
326
+
327
+ Gives you:
328
+ - Type-check + diagnostics for attribute *values* inside `` html`` `` templates.
329
+ - Go-to-definition from `<my-counter>` to the class registered via `MyCounter.register('my-counter')`.
330
+ - Diagnostic suppression for "Unknown tag" / "Unknown attribute" on tags any webjs class registers.
331
+ - Attribute auto-complete from `static properties = { … }` keys.
332
+ - Attribute-value type-check: `<my-counter count=${expr}>` assignability-checks `typeof expr` against `declare count: T`.
333
+
334
+ Both behaviours are gated on import-graph reachability: a tag is recognized only if the file registering it is reachable from the file you're editing.
@@ -6,10 +6,12 @@ node_modules
6
6
  # `.webjs/` is ignored EXCEPT for `.webjs/vendor/`, which holds the committed
7
7
  # importmap manifest (and optionally downloaded bundle bytes) the server needs
8
8
  # at boot without reaching api.jspm.io. DO NOT collapse to `**/.webjs`: parent
9
- # exclusion blocks child negations and the vendor files would never ship.
10
- .webjs/*
11
- !.webjs/vendor/
12
- !.webjs/vendor/**
9
+ # exclusion blocks child negations and the vendor files would never ship. The
10
+ # `**/` prefix matches `.webjs/` at any depth so a nested build context does
11
+ # not ship the per-machine cache, mirroring the `.gitignore` pattern.
12
+ **/.webjs/*
13
+ !**/.webjs/vendor/
14
+ !**/.webjs/vendor/**
13
15
 
14
16
  dist
15
17
  build
@@ -14,7 +14,19 @@ now (`app/page.ts` printing "Hello from {{APP_NAME}}", the example `User`
14
14
  model in `prisma/schema.prisma`, the `theme-toggle` component, the
15
15
  example users module in api/saas templates) are **starting-point
16
16
  references, not the final product**. Your job is to replace them with
17
- the app the user actually asked for.
17
+ the app the user actually asked for. That includes adapting
18
+ `app/layout.ts`, not just the page. Set the real brand, replace the
19
+ example `Home` nav, and pick a content-width container that fits. The
20
+ default `<main class="max-w-[760px]">` is a reading column for prose and
21
+ forms, so for a full-bleed app, dashboard, or board, widen the cap or
22
+ remove it (keep the theme tokens). A wide layout left in the 760px
23
+ reading column overflows into a horizontal scrollbar. This is ENFORCED:
24
+ the example `app/page.ts` and `app/layout.ts` carry a
25
+ `webjs-scaffold-placeholder` marker comment, and `webjs check` fails
26
+ while any marker remains, so this freshly scaffolded app fails the check
27
+ until you replace the example content (or deliberately keep it) and
28
+ delete the marker line. The delivered app must contain only what the
29
+ user asked for, never leftover scaffold code.
18
30
 
19
31
  **Non-negotiables for every webjs app:**
20
32
 
@@ -449,15 +461,18 @@ URLs or transitive deps drift. Pin is a deliberate developer action,
449
461
  like `npm install` itself.
450
462
 
451
463
  **Do NOT modify the `.webjs/` lines in `.gitignore` / `.dockerignore`.**
452
- The scaffolded pattern is three lines (`.webjs/*` + `!.webjs/vendor/`
453
- + `!.webjs/vendor/**`) and is structurally load-bearing. Collapsing it
454
- to a single `.webjs/` excludes the parent directory; once the parent
455
- is excluded, git cannot re-include `.webjs/vendor/` via a child
456
- negation (gitignore semantics: parent exclusion blocks child
457
- negations). The breakage is invisible: `webjs vendor pin` runs, writes
458
- files, and git silently ignores them. Production then has no
459
- importmap.json and the server falls back to calling api.jspm.io on
460
- every cold start. The `gitignore-vendor-not-ignored` lint rule
464
+ The scaffolded `.gitignore` pattern is three lines (`**/.webjs/*` +
465
+ `!**/.webjs/vendor/` + `!**/.webjs/vendor/**`) and is structurally
466
+ load-bearing. Collapsing it to a single `.webjs/` excludes the parent
467
+ directory; once the parent is excluded, git cannot re-include
468
+ `.webjs/vendor/` via a child negation (gitignore semantics: parent
469
+ exclusion blocks child negations). The breakage is invisible: `webjs
470
+ vendor pin` runs, writes files, and git silently ignores them.
471
+ Production then has no importmap.json and the server falls back to
472
+ calling api.jspm.io on every cold start. The `**/` prefix matters too:
473
+ it ignores `.webjs/` at any depth, so an app nested below its repo root
474
+ (a monorepo package) does not leak its generated `.webjs/routes.d.ts`
475
+ into `git status`. The `gitignore-vendor-not-ignored` lint rule
461
476
  (`webjs check`) verifies the pattern with `git check-ignore` and will
462
477
  fail CI if it regresses.
463
478
 
@@ -308,11 +308,28 @@ When the user asks the agent to build their actual app:
308
308
  need a theme picker.
309
309
  4. **Delete the example users module** (api/saas templates) if the app
310
310
  doesn't use it.
311
- 5. **Keep:** the Prisma setup, the test config, the agent config files
311
+ 5. **Adapt `app/layout.ts` to the app, not just the page.** Set the real
312
+ brand, replace the example `Home` nav with the app's navigation, and
313
+ pick a content-width container that fits. The default
314
+ `<main class="max-w-[760px]">` is a reading column for prose, forms,
315
+ and marketing. Widen it or drop the cap for a full-bleed app,
316
+ dashboard, or board, or a wide layout overflows into an unnecessary
317
+ horizontal scrollbar. Keep the design tokens and theme setup, those
318
+ are infrastructure.
319
+ 6. **Keep:** the Prisma setup, the test config, the agent config files
312
320
  (`AGENTS.md`, `CONVENTIONS.md`, `CLAUDE.md`, `.cursorrules`, etc.),
313
321
  `lib/prisma.server.ts`, the directory conventions, the design tokens in
314
322
  `app/layout.ts`. These are the infrastructure, not the example app.
315
323
 
324
+ This is enforced, not just advised. The example `app/page.ts` and
325
+ `app/layout.ts` carry a `webjs-scaffold-placeholder` marker comment, and
326
+ the `no-scaffold-placeholder` check fails while any marker remains, so a
327
+ freshly scaffolded app fails `webjs check` until you address each
328
+ placeholder. The marker is acknowledge-and-remove: replace the example
329
+ content, or deliberately keep it, and in either case delete the marker
330
+ line. So the delivered app contains only what the user asked for, never
331
+ leftover scaffold code.
332
+
316
333
  The scaffold exists so the agent doesn't reinvent the directory layout,
317
334
  the Prisma wiring, the test runner config, or the convention files. It
318
335
  does NOT exist so the agent ships the example homepage.