Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ Thumbs.db
# editors
.vscode/
.idea/
# Exception: the scaffold ships a VS Code settings file (the webjs-config
# JSON Schema association, #259) that MUST be committed so `webjs create`
# copies it into new apps. The repo's own top-level .vscode/ stays ignored.
!packages/cli/templates/.vscode/
!packages/cli/templates/.vscode/settings.json

# AI assistants - local session state, scheduled-task locks, etc.
# Keep repo-shared config (settings.json + hooks scripts) tracked so
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ The bare `@webjsdev/core` specifier resolves to a BROWSER bundle that drops serv
| `WebjsFrame` (`<webjs-frame id="...">`) | Escape-hatch partial-swap region. A frame nav whose response lacks the frame fires a cancelable, bubbling `webjs:frame-missing` event (detail `{ frameId, url, document }`) and leaves the frame unchanged instead of full-swapping; `preventDefault()` hands the outcome to the listener. |
| `Metadata` / `MetadataContext` (type-only) | Types the `metadata` / `generateMetadata(ctx)` return + context. `import type { Metadata } from '@webjsdev/core'`. |
| `PageProps<R>` / `LayoutProps<R>` / `RouteHandlerContext<R>` (type-only) | Types the page / layout / route-handler args (`{ params, searchParams, url, actionData }`; layouts add `children`). `R` is an optional route literal that narrows `params` against the generated route union. `Route` / `RouteParams<R>` are the href + params helpers. Run `webjs types` to generate the union (see CLI reference). `import type { PageProps } from '@webjsdev/core'`. |
| `WebjsConfig` (type-only) | Types the `webjs` package.json config block (`elide`, `headers`, `redirects`, `trailingSlash`, `csp`, the ingress body-size + timeout caps), with `WebjsHeaderRule` / `WebjsRedirectRule` / `WebjsCspConfig` / `WebjsTrailingSlash` for the nested shapes. A companion JSON Schema (`@webjsdev/server/webjs-config.schema.json`, associated in the scaffold's `.vscode/settings.json`) flags an unknown key in the editor. `import type { WebjsConfig } from '@webjsdev/core'`. |

### Directives, from `import { … } from '@webjsdev/core/directives'`

Expand Down
35 changes: 35 additions & 0 deletions agent-docs/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,41 @@ bundler. The mechanism is `generateRouteTypes(appDir)` in
(`buildRouteTable`). Output is deterministic (sorted keys), so re-running
yields a byte-identical file.

### The `webjs` package.json config block: `WebjsConfig` + JSON Schema

The `webjs` object in `package.json` (the `elide` / `headers` / `redirects` /
`trailingSlash` / `csp` knobs plus the ingress body-size and timeout caps) has
two typed references, so a typo'd key is diagnosed instead of silently dropped:

- **A JSON Schema**, `packages/server/webjs-config.schema.json` (shipped in the
`@webjsdev/server` package). The scaffold's `.vscode/settings.json` associates
it with the `webjs` property of `package.json`, so VS Code flags an unknown
key natively while you edit the JSON. `additionalProperties: false` on the
block is what turns a typo into an editor warning.
- **The `WebjsConfig` type**, exported from `@webjsdev/core`, a typed reference
for an agent or human authoring the block (with `WebjsHeaderRule`,
`WebjsRedirectRule`, `WebjsCspConfig`, `WebjsTrailingSlash` for the nested
shapes).

```ts
import type { WebjsConfig } from '@webjsdev/core';

const config: WebjsConfig = {
trailingSlash: 'never',
csp: true,
redirects: [{ source: '/old', destination: '/new' }],
};
```

The schema and the type mirror what the server readers actually consume
(`readElideEnabled`, `compileHeaderRules`, `compileRedirectRules` /
`readTrailingSlashPolicy`, `readCspConfig`, `readBodyLimits` /
`computeServerTimeouts`). Adding a `webjs.*` key means updating the schema, the
type, AND the reader in lockstep, the one procedure documented in
`packages/server/AGENTS.md`. A drift test
(`packages/server/test/config/webjs-config-schema.test.js`) fails if the schema
and the reader key set diverge.

### TypeScript is not required

JS + JSDoc gets the same call-site type safety. The TypeScript language
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/lib/create.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
// to main, mirroring the webjs framework's own CI.
'.github/workflows/ci.yml',
'.editorconfig',
// VS Code: associate the published webjs-config JSON Schema with the
// package.json `webjs` block, so an unknown / typo'd key (#259) is
// flagged natively in the editor instead of silently dropped.
'.vscode/settings.json',
// Production / deploy scaffolding. `docker compose up --build` runs
// the app locally with the same Dockerfile production builds from.
'Dockerfile',
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/templates/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ npm-debug.log*
Thumbs.db

# editors
.vscode/
# `.vscode/*` ignored, but `.vscode/settings.json` (the webjs-config JSON
# Schema association, #259) is committed so the editor validates package.json's
# webjs block out of the box. Same gitignore shape as `.webjs/*` above: a bare
# `.vscode/` would exclude the directory and no negation could re-include a
# child, so the settings file would silently never ship.
.vscode/*
!.vscode/settings.json
.idea/

# test artifacts
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/templates/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"json.schemas": [
{
"fileMatch": ["/package.json"],
"schema": {
"type": "object",
"properties": {
"webjs": {
"$ref": "./node_modules/@webjsdev/server/webjs-config.schema.json"
}
}
}
}
]
}
9 changes: 9 additions & 0 deletions packages/cli/templates/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ layered on top:
See [docs.webjs.com → Editor setup](https://docs.webjs.com/docs/editor-setup)
for the full walkthrough.

**Config validation in `package.json`.** The scaffold ships
`.vscode/settings.json`, which associates the published webjs-config JSON
Schema (`@webjsdev/server/webjs-config.schema.json`) with the `webjs` block
of `package.json`. In VS Code an unknown / typo'd `webjs.*` key (`redirect`
for `redirects`, say) is then flagged inline instead of silently dropped to
the default. The same shape is typed by the `WebjsConfig` type from
`@webjsdev/core` (`import type { WebjsConfig } from '@webjsdev/core'`) for a
typed reference.

## UI components: Webjs UI (preinstalled)

This scaffold ships with the standard Webjs UI component kit
Expand Down
8 changes: 7 additions & 1 deletion packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@ typing lives in `src/component.d.ts`; the page-metadata typing
`src/metadata.d.ts`; the typed page / layout / route-handler props plus the
opt-in route union (`PageProps`, `LayoutProps`, `RouteHandlerContext`, `Route`,
`RouteParams`, and the `WebjsRoutes` / `RouteParamMap` augmentation targets,
#258) live in `src/routes.d.ts`. All are pure declaration files (erased at
#258) live in `src/routes.d.ts`. The `webjs` package.json config-block typing
(`WebjsConfig` plus the nested `WebjsHeaderRule` / `WebjsRedirectRule` /
`WebjsCspConfig` / `WebjsTrailingSlash`, #259) lives in
`src/webjs-config.d.ts`; it mirrors the `@webjsdev/server` config readers and
the companion JSON Schema (`packages/server/webjs-config.schema.json`), and
those three MUST stay in lockstep (the procedure is documented in
`packages/server/AGENTS.md`). All are pure declaration files (erased at
runtime, zero build cost). A page imports them with `import type { Metadata,
PageProps } from '@webjsdev/core'`. The `Metadata` and `PageProps` /
`LayoutProps` shapes MUST stay in lockstep with what
Expand Down
13 changes: 13 additions & 0 deletions packages/core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,19 @@ export type {
RouteParamMap,
} from './src/routes.d.ts';

// The package.json `webjs` config block (#259). Typed reference for the
// elide / headers / redirects / trailingSlash / csp / ingress-limit knobs;
// the companion JSON Schema (packages/server/webjs-config.schema.json) gives
// editors native validation of package.json itself.
export type {
WebjsConfig,
WebjsHeaderRule,
WebjsHeaderDirective,
WebjsRedirectRule,
WebjsTrailingSlash,
WebjsCspConfig,
} from './src/webjs-config.d.ts';

export { html, isTemplate, MARKER } from './src/html.js';
export { css, isCSS, adoptStyles, stylesToString } from './src/css.js';
export { register, lookup, lookupModuleUrl, isLazy, allTags, primeModuleUrl, tagOf } from './src/registry.js';
Expand Down
165 changes: 165 additions & 0 deletions packages/core/src/webjs-config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* TypeScript overlay for the `webjs` config block in a webjs app's
* package.json.
*
* // package.json is JSON, so author it there, but a typed reference
* // helps an agent or a human author the block correctly:
* import type { WebjsConfig } from '@webjsdev/core';
* const config: WebjsConfig = { trailingSlash: 'never', csp: true };
*
* The server reads this object key by key. Without a type or schema a
* typo'd key (e.g. `redirect` for `redirects`) was silently dropped and
* the feature stayed at its default with no diagnostic. This type plus
* the published JSON Schema close that gap.
*
* LOCKSTEP: this file, the JSON Schema at
* packages/server/webjs-config.schema.json, and the server reader
* functions MUST stay in sync. The readers are: readElideEnabled
* (dev.js, elide), compileHeaderRules (headers.js, headers),
* compileRedirectRules / readTrailingSlashPolicy (redirects.js,
* redirects / trailingSlash), readCspConfig (csp.js, csp), and
* readBodyLimits / computeServerTimeouts (body-limit.js, the byte caps
* and timeouts). Adding a `webjs.*` key means updating all three places.
* See packages/server/AGENTS.md for the one documented procedure.
*
* Every key is optional (the whole block is optional and every key has a
* default). Zero runtime cost: nothing in this file ships to the browser.
*/

/** One header directive in a `webjs.headers` rule. */
export interface WebjsHeaderDirective {
/** Header name, e.g. `X-Frame-Options`. */
key: string;
/**
* Header value. A `null` or `false` value REMOVES the header on a
* match, the escape hatch that drops a secure default on a path.
* `true` is intentionally not allowed (the runtime would stringify it
* to the literal `"true"`, which is never a useful header value).
*/
value?: string | null | false;
}

/** One per-path response-header rule in `webjs.headers`. */
export interface WebjsHeaderRule {
/**
* Path pattern matched with the native URLPattern API, so `:param` and
* `:rest*` syntax works.
*/
source: string;
/** Header directives applied on a match. */
headers: WebjsHeaderDirective[];
}

/** One declarative redirect rule in `webjs.redirects`. */
export interface WebjsRedirectRule {
/**
* Path pattern matched with the native URLPattern API, so `:param` and
* `:rest*` syntax works.
*/
source: string;
/**
* Target path, a path referencing named groups captured by `source`,
* or an absolute URL. The incoming query string is preserved and
* merged onto the destination.
*/
destination: string;
/**
* `true` (the default) is a 308 Permanent Redirect, `false` is a 307
* Temporary Redirect. Both preserve the request method and body.
* `statusCode` wins over this when set.
*/
permanent?: boolean;
/**
* Explicit redirect status, for a tool needing a legacy code. Wins
* over `permanent`. One of 301, 302, 303, 307, 308.
*/
statusCode?: 301 | 302 | 303 | 307 | 308;
}

/** The trailing-slash canonicalization policy in `webjs.trailingSlash`. */
export type WebjsTrailingSlash = 'never' | 'always' | 'ignore';

/** The object form of `webjs.csp` (the non-boolean shape). */
export interface WebjsCspConfig {
/**
* Directive map merged over the strict defaults, e.g.
* `{ 'connect-src': "'self' https://api.example.com" }`. A `null` /
* `false` / `''` value drops a default directive. A `__NONCE__` token
* in a value is replaced with the per-request nonce.
*/
directives?: Record<string, string | null | boolean>;
/**
* `true` emits `Content-Security-Policy-Report-Only` instead of the
* enforcing header (the staged-rollout path).
*/
reportOnly?: boolean;
}

/**
* The `webjs` object in a webjs app's package.json. Every key is
* optional. Mirrors what the server readers actually consume, NOT a
* Next.js superset.
*/
export interface WebjsConfig {
/**
* Display-only and inert-route dead-JS elision switch. Default `true`.
* Set to `false` to ship every module's JS app-wide. The `WEBJS_ELIDE`
* env override wins over this.
*/
elide?: boolean;

/** Per-path response-header rules, shaped like Next's. */
headers?: WebjsHeaderRule[];

/** Declarative permanent / temporary redirects for moved URLs. */
redirects?: WebjsRedirectRule[];

/**
* Trailing-slash canonicalization policy. Default `'ignore'` (no-op).
* `'never'` strips a trailing slash, `'always'` adds one (both via a
* 308 redirect).
*/
trailingSlash?: WebjsTrailingSlash;

/**
* Content-Security-Policy config. Off by default. `true` enables a
* strict nonce-based default policy. An object customizes directives
* and report-only mode.
*/
csp?: boolean | WebjsCspConfig;

/**
* JSON / RPC request body cap in bytes. Default 1048576 (1 MiB). `0`
* disables the cap. The `WEBJS_MAX_BODY_BYTES` env override wins.
*/
maxBodyBytes?: number;

/**
* Form / multipart request body cap in bytes. Default 10485760 (10
* MiB). `0` disables the cap. The `WEBJS_MAX_MULTIPART_BYTES` env
* override wins.
*/
maxMultipartBytes?: number;

/**
* Max time in ms to receive the ENTIRE request (headers plus body).
* Default 30000. `0` disables the timeout. The
* `WEBJS_REQUEST_TIMEOUT_MS` env override wins.
*/
requestTimeoutMs?: number;

/**
* Max time in ms to receive just the request headers. Default 20000.
* Clamped strictly under `requestTimeoutMs` per node semantics. `0`
* disables the timeout. The `WEBJS_HEADERS_TIMEOUT_MS` env override
* wins.
*/
headersTimeoutMs?: number;

/**
* Idle time in ms before a kept-alive socket is closed. Default 5000.
* `0` disables the timeout. The `WEBJS_KEEP_ALIVE_TIMEOUT_MS` env
* override wins.
*/
keepAliveTimeoutMs?: number;
}
33 changes: 32 additions & 1 deletion packages/server/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,38 @@ with metadata, Suspense, streaming) for HTML, or `api.js` /

See [`index.js`](./index.js) and [`package.json` exports](./package.json).
The `./check` subpath is exported separately so the CLI's `webjs check`
can load it without booting the full server.
can load it without booting the full server. The
`./webjs-config.schema.json` subpath publishes the config JSON Schema
(below) so an editor can resolve it from
`node_modules/@webjsdev/server/webjs-config.schema.json`.

## The `webjs` package.json config block (typed surface, #259)

The `webjs.*` keys an app sets in `package.json` are typed and validated
in THREE co-located places that MUST stay in lockstep:

1. **The JSON Schema**, [`webjs-config.schema.json`](./webjs-config.schema.json)
(in this package's `files` allowlist + `exports`). `additionalProperties:
false` flags an unknown / typo'd key; the scaffold's `.vscode/settings.json`
associates it with the `webjs` property of `package.json` so VS Code
validates the block natively. This is the primary "fails-open -> diagnosed"
fix: a typo used to be silently dropped to the default.
2. **The TS type** `WebjsConfig` in
`packages/core/src/webjs-config.d.ts` (re-exported from
`@webjsdev/core`), the typed reference an agent or human authors against.
3. **The reader functions** that consume each key: `readElideEnabled`
(`dev.js`, `elide`), `compileHeaderRules` (`headers.js`, `headers`),
`compileRedirectRules` / `readTrailingSlashPolicy` (`redirects.js`,
`redirects` / `trailingSlash`), `readCspConfig` (`csp.js`, `csp`), and
`readBodyLimits` / `computeServerTimeouts` (`body-limit.js`, the byte
caps + timeouts).

**To add or change a `webjs.*` key, update all three (schema + type +
reader), and the `KNOWN_KEYS` list in the drift test.** The drift test
`test/config/webjs-config-schema.test.js` asserts the schema property set
and the reader key set never diverge (a counterfactual unknown key proves
`additionalProperties:false` would flag it); the type fixture
`test/types/webjs-config.test-d.ts` asserts the type matches.

## Package-specific invariants

Expand Down
4 changes: 3 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
"main": "index.js",
"exports": {
".": "./index.js",
"./check": "./src/check.js"
"./check": "./src/check.js",
"./webjs-config.schema.json": "./webjs-config.schema.json"
},
"files": [
"index.js",
"src",
"webjs-config.schema.json",
"README.md"
],
"dependencies": {
Expand Down
Loading