Compare commits
182
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f6552ea62 | ||
|
|
5bc391e74f | ||
|
|
3327ec0d37 | ||
|
|
ceb865f4f1 | ||
|
|
45309b3f72 | ||
|
|
8f3a13c854 | ||
|
|
eca4447dcf | ||
|
|
904bca21b2 | ||
|
|
b98d417185 | ||
|
|
7d737ec0a0 | ||
|
|
f5538ed621 | ||
|
|
d2d830e7e2 | ||
|
|
0618782355 | ||
|
|
9067e68f69 | ||
|
|
9d64ec60c4 | ||
|
|
fc1eea3939 | ||
|
|
4ec5dad60a | ||
|
|
a1a2947910 | ||
|
|
270040fb5f | ||
|
|
f9e285b182 | ||
|
|
3b402b4e36 | ||
|
|
fa535575c5 | ||
|
|
46ef4b775b | ||
|
|
7a36c1905f | ||
|
|
1a8c75a958 | ||
|
|
b3c93e9b18 | ||
|
|
613ff7ae5b | ||
|
|
e372ae571a | ||
|
|
354082ebc3 | ||
|
|
aa21bc4a18 | ||
|
|
1be482caab | ||
|
|
2d1cda0eab | ||
|
|
d26403fce2 | ||
|
|
f52e1d50e4 | ||
|
|
fe44bc2091 | ||
|
|
98e0813061 | ||
|
|
a9670c2a1c | ||
|
|
1a94179b5f | ||
|
|
f13e83fd74 | ||
|
|
7351ed1566 | ||
|
|
53b9947ef9 | ||
|
|
56cde5aaf8 | ||
|
|
4be4b2c346 | ||
|
|
6258495b67 | ||
|
|
631e1a2ddb | ||
|
|
2e12656060 | ||
|
|
d87b197224 | ||
|
|
64ab20cc95 | ||
|
|
46195462c3 | ||
|
|
8fb96f521f | ||
|
|
9377a69d88 | ||
|
|
9332522614 | ||
|
|
37439188ca | ||
|
|
7c5069b5b5 | ||
|
|
41b7eb8798 | ||
|
|
0be69fb234 | ||
|
|
bab11083c6 | ||
|
|
a3ddd39b7b | ||
|
|
96e082b943 | ||
|
|
2299a0726d | ||
|
|
2d0df4efc9 | ||
|
|
20783699ac | ||
|
|
d8305a5a14 | ||
|
|
98a46091b5 | ||
|
|
2c50a07ed2 | ||
|
|
9746e8e875 | ||
|
|
0ddb481159 | ||
|
|
3441b96362 | ||
|
|
84e58b1798 | ||
|
|
de4477dd9c | ||
|
|
2970d5fff3 | ||
|
|
eeef2d79df | ||
|
|
ef6de302e9 | ||
|
|
a970d04248 | ||
|
|
5429057a38 | ||
|
|
e944fd4496 | ||
|
|
861444b8a3 | ||
|
|
ba957e861e | ||
|
|
29febb2e0c | ||
|
|
cd0dffa87d | ||
|
|
f57bd05a03 | ||
|
|
2bb52487eb | ||
|
|
990a8128a7 | ||
|
|
7a1b4e5b33 | ||
|
|
aded2daab9 | ||
|
|
6bb3ab5fe7 | ||
|
|
4aa0973352 | ||
|
|
e616ed276e | ||
|
|
74490964ee | ||
|
|
7a3e55b150 | ||
|
|
fb24cc7ec3 | ||
|
|
63316111cb | ||
|
|
0ed8351828 | ||
|
|
890d6106b3 | ||
|
|
de99a2c2e0 | ||
|
|
442c058d0c | ||
|
|
a08dfa5322 | ||
|
|
712a6d3d8c | ||
|
|
9dec811069 | ||
|
|
847b7010d1 | ||
|
|
1dbe16dc93 | ||
|
|
f32b33e3b6 | ||
|
|
768074ac0a | ||
|
|
c7e40154ca | ||
|
|
680ea73975 | ||
|
|
953b1cd692 | ||
|
|
87a00de5f3 | ||
|
|
d069f5ddd7 | ||
|
|
abebbcf8af | ||
|
|
5720f93db1 | ||
|
|
7f5e1bc3cf | ||
|
|
97d60d0ba8 | ||
|
|
e7e7b58160 | ||
|
|
e09a35b4bd | ||
|
|
5c8f3ede54 | ||
|
|
d19595c7ba | ||
|
|
b7796b103a | ||
|
|
ec63090006 | ||
|
|
224af8fd96 | ||
|
|
68cc75d0b0 | ||
|
|
43652c14af | ||
|
|
e40d8319a6 | ||
|
|
2f075df42c | ||
|
|
281615a4b0 | ||
|
|
3ae5d7cf97 | ||
|
|
b5029889a5 | ||
|
|
e9db4ca24d | ||
|
|
5318320c70 | ||
|
|
b3ed9689fa | ||
|
|
e5de7b54a5 | ||
|
|
3252b1b20e | ||
|
|
7601477f7d | ||
|
|
847b6dbe59 | ||
|
|
04be24ddd8 | ||
|
|
785e8a65bd | ||
|
|
a2698fb51a | ||
|
|
419614d9d1 | ||
|
|
70777e4a45 | ||
|
|
bd2f6ac5e3 | ||
|
|
b457ad1d54 | ||
|
|
323f57b32b | ||
|
|
028c2a6d64 | ||
|
|
2f0f82b29f | ||
|
|
50097ec4b4 | ||
|
|
f026415ba6 | ||
|
|
2cbc3e43e1 | ||
|
|
55fed2177a | ||
|
|
8c609edd32 | ||
|
|
e898929193 | ||
|
|
18a1c40118 | ||
|
|
a20f143acb | ||
|
|
ac248f2bb0 | ||
|
|
0904a4efaa | ||
|
|
5b11b937bb | ||
|
|
f199385204 | ||
|
|
c0c2fa4595 | ||
|
|
cfdcdd00ad | ||
|
|
03d5cb6aa6 | ||
|
|
701acd828c | ||
|
|
b28b79c370 | ||
|
|
fdd0c9f847 | ||
|
|
7ad336b4dd | ||
|
|
d70e89230b | ||
|
|
7e6d8c3bc8 | ||
|
|
92c0920c9b | ||
|
|
a8d8ac386f | ||
|
|
2c8841cc5f | ||
|
|
b344d2a70a | ||
|
|
e83f0366ef | ||
|
|
d9e8f5be82 | ||
|
|
bd0c1317ff | ||
|
|
97801287f9 | ||
|
|
d77638131b | ||
|
|
609224591c | ||
|
|
6074d19c43 | ||
|
|
7301849a7f | ||
|
|
7d481df652 | ||
|
|
5477f5436d | ||
|
|
b646ec8d00 | ||
|
|
e66d2425aa | ||
|
|
b3b65dddd8 | ||
|
|
5dbcc5b85d |
@@ -13,6 +13,12 @@
|
|||||||
],
|
],
|
||||||
"port": 3520
|
"port": 3520
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sendline",
|
||||||
|
"runtimeExecutable": "bun",
|
||||||
|
"runtimeArgs": ["run", "--cwd", "D:/Company/sendline", "dev", "--port=3610"],
|
||||||
|
"port": 3610
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "component-showcase",
|
"name": "component-showcase",
|
||||||
"runtimeExecutable": "bun",
|
"runtimeExecutable": "bun",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.3.14
|
bun-version: 1.4.0
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|||||||
@@ -25,3 +25,6 @@ tsconfig.focus.json
|
|||||||
# "@wrnexus/*" bare specifiers (resolved via the root tsconfig.json `paths`,
|
# "@wrnexus/*" bare specifiers (resolved via the root tsconfig.json `paths`,
|
||||||
# which requires the scaffold to live inside the repo tree).
|
# which requires the scaffold to live inside the repo tree).
|
||||||
**/test/.tmp-*/
|
**/test/.tmp-*/
|
||||||
|
|
||||||
|
# Subagent-driven-development scratch (ledger, briefs, review packages)
|
||||||
|
.superpowers/
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ bun.lockb
|
|||||||
# Generated code (queries.gen.ts, routes.gen.ts, etc.)
|
# Generated code (queries.gen.ts, routes.gen.ts, etc.)
|
||||||
**/*.gen.ts
|
**/*.gen.ts
|
||||||
**/*.generated.d.ts
|
**/*.generated.d.ts
|
||||||
|
**/*.generated.api-checks.ts
|
||||||
|
|
||||||
# Bundled .wrn compiler for the VS Code extension (generated)
|
# Bundled .wrn compiler for the VS Code extension (generated)
|
||||||
editors/vscode/src/compiler.cjs
|
editors/vscode/src/compiler.cjs
|
||||||
|
|||||||
@@ -1,5 +1,44 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- Added framework-owned authentication and authorization integration: lazy auth stores,
|
||||||
|
configured OAuth routes with cookie-bound transactions, production configuration validation,
|
||||||
|
default lifecycle roles, typed request users, declarative API permissions, page guards, and
|
||||||
|
consistent unauthorized/forbidden responses.
|
||||||
|
- Added reusable application primitives for typed route params, bounded cursor pagination,
|
||||||
|
owned-resource authorization, atomic database state transitions, queue batching and explicit
|
||||||
|
worker lifecycles.
|
||||||
|
- Expanded `@wrnexus/test` with official typed contexts, Bun-compatible fetch mocking and a
|
||||||
|
full-stack harness alias; made generated files deterministic and introduced `wrnexus check` as
|
||||||
|
the canonical build-before-check command for generated applications.
|
||||||
|
|
||||||
|
- Fixed `v.boolean()` coercion in `@wrnexus/validation` (`checkField` in both `src/index.ts`
|
||||||
|
and the browser mirror in `src/runtime.ts`): previously any string other than `"true"` or
|
||||||
|
`"on"` silently coerced to `false` with no error, so typos and unrecognised values (e.g.
|
||||||
|
`"yes"`, `"1"`, `"TRUE"`, `"treu"`) passed validation as a silent, wrong `false`. Now
|
||||||
|
recognised true strings (`"true"`, `"on"`, `"1"`, `"yes"`, case-insensitive and trimmed) and
|
||||||
|
false strings (`"false"`, `"off"`, `"0"`, `"no"`) coerce as expected, numeric `1`/`0` coerce
|
||||||
|
(for JSON payloads), and absent/empty input (`undefined`/`null`/`""`) still coerces to
|
||||||
|
`false` exactly as before (unchanged HTML-checkbox semantics). **Behavior change for
|
||||||
|
downstream apps:** any other value — an unrecognised string, an object, an array — is now a
|
||||||
|
type error (`desc.typeMessage` or "Must be true or false") instead of a silent `false`. A
|
||||||
|
required boolean field given `false` still errors, as before (checkbox-required semantics
|
||||||
|
are unchanged). A repo-wide search of `packages/`, `examples/`, and `services/` found no
|
||||||
|
existing `v.boolean()` usage that feeds an unrecognised value, so no call sites are expected
|
||||||
|
to start failing.
|
||||||
|
|
||||||
|
- Fixed `defineEndpoint` (`@wrnexus/core`) so routes invoked through the real HTTP router
|
||||||
|
(which calls handlers as `handler(ctx)`, with no second argument) actually receive their
|
||||||
|
request input: it now parses query parameters for GET/HEAD and the JSON body otherwise
|
||||||
|
when no input is passed explicitly. Previously such endpoints silently validated
|
||||||
|
`undefined`, so an `input` schema with only optional fields passed vacuously regardless of
|
||||||
|
what was sent. **Behavior change for downstream apps:** a request that previously passed
|
||||||
|
vacuous validation on a `defineEndpoint` route can now legitimately fail (400
|
||||||
|
`VALIDATION_ERROR`) if it does not actually satisfy the schema. Explicitly passing a second
|
||||||
|
argument (e.g. from a unit test or an internal caller) is unaffected and still takes
|
||||||
|
priority over reading the request.
|
||||||
|
|
||||||
## 0.8.8
|
## 0.8.8
|
||||||
|
|
||||||
- Added the framework request context to `.wrn` language-server type environments.
|
- Added the framework request context to `.wrn` language-server type environments.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.4.0",
|
||||||
"@types/react": "^19.2.18",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.4",
|
"@types/react-dom": "^19.2.4",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
@@ -93,6 +93,25 @@
|
|||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"examples/crm-app": {
|
||||||
|
"name": "wrnexus-crm-example",
|
||||||
|
"version": "0.8.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@wrnexus/auth": "workspace:*",
|
||||||
|
"@wrnexus/authz": "workspace:*",
|
||||||
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/styles": "workspace:*",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.123",
|
||||||
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
|
"@types/bun": "^1.3.14",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
"examples/i18n-showcase": {
|
"examples/i18n-showcase": {
|
||||||
"name": "i18n-showcase",
|
"name": "i18n-showcase",
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
@@ -114,7 +133,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@wrnexus/cli": "^0.8.35",
|
"@wrnexus/cli": "^0.8.36",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -126,27 +145,27 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/ai": "^0.8.9",
|
"@wrnexus/ai": "^0.8.9",
|
||||||
"@wrnexus/auth": "^0.8.11",
|
"@wrnexus/auth": "^0.8.12",
|
||||||
"@wrnexus/authz": "^0.8.9",
|
"@wrnexus/authz": "^0.8.9",
|
||||||
"@wrnexus/captcha": "^0.8.10",
|
"@wrnexus/captcha": "^0.8.11",
|
||||||
"@wrnexus/core": "^0.8.9",
|
"@wrnexus/core": "^0.8.9",
|
||||||
"@wrnexus/csr": "^0.8.21",
|
"@wrnexus/csr": "^0.8.21",
|
||||||
"@wrnexus/db": "^0.8.14",
|
"@wrnexus/db": "^0.8.15",
|
||||||
"@wrnexus/dev-server": "^0.8.32",
|
"@wrnexus/dev-server": "^0.8.33",
|
||||||
"@wrnexus/encryption": "^0.8.8",
|
"@wrnexus/encryption": "^0.8.9",
|
||||||
"@wrnexus/helpers": "^0.8.8",
|
"@wrnexus/helpers": "^0.8.8",
|
||||||
"@wrnexus/i18n": "^0.8.10",
|
"@wrnexus/i18n": "^0.8.11",
|
||||||
"@wrnexus/image": "^0.8.9",
|
"@wrnexus/image": "^0.8.10",
|
||||||
"@wrnexus/jwt": "^0.8.8",
|
"@wrnexus/jwt": "^0.8.9",
|
||||||
"@wrnexus/observability": "^0.8.8",
|
"@wrnexus/observability": "^0.8.8",
|
||||||
"@wrnexus/realtime": "^0.8.9",
|
"@wrnexus/realtime": "^0.8.10",
|
||||||
"@wrnexus/security": "^0.8.8",
|
"@wrnexus/security": "^0.8.8",
|
||||||
"@wrnexus/store": "^0.8.8",
|
"@wrnexus/store": "^0.8.8",
|
||||||
"@wrnexus/styles": "^0.8.15",
|
"@wrnexus/styles": "^0.8.15",
|
||||||
"@wrnexus/tracking": "^0.8.8",
|
"@wrnexus/tracking": "^0.8.8",
|
||||||
"@wrnexus/ui": "^0.8.19",
|
"@wrnexus/ui": "^0.8.19",
|
||||||
"@wrnexus/uploader": "^0.8.9",
|
"@wrnexus/uploader": "^0.8.10",
|
||||||
"@wrnexus/validation": "^0.8.9",
|
"@wrnexus/validation": "^0.8.10",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -154,7 +173,7 @@
|
|||||||
"@iconify/tailwind4": "^1.2.3",
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
"@tailwindcss/cli": "^4.3.3",
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@wrnexus/cli": "^0.8.35",
|
"@wrnexus/cli": "^0.8.36",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
@@ -167,27 +186,27 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/ai": "^0.8.9",
|
"@wrnexus/ai": "^0.8.9",
|
||||||
"@wrnexus/auth": "^0.8.11",
|
"@wrnexus/auth": "^0.8.12",
|
||||||
"@wrnexus/authz": "^0.8.9",
|
"@wrnexus/authz": "^0.8.9",
|
||||||
"@wrnexus/captcha": "^0.8.10",
|
"@wrnexus/captcha": "^0.8.11",
|
||||||
"@wrnexus/core": "^0.8.9",
|
"@wrnexus/core": "^0.8.9",
|
||||||
"@wrnexus/csr": "^0.8.21",
|
"@wrnexus/csr": "^0.8.21",
|
||||||
"@wrnexus/db": "^0.8.14",
|
"@wrnexus/db": "^0.8.15",
|
||||||
"@wrnexus/dev-server": "^0.8.32",
|
"@wrnexus/dev-server": "^0.8.33",
|
||||||
"@wrnexus/encryption": "^0.8.8",
|
"@wrnexus/encryption": "^0.8.9",
|
||||||
"@wrnexus/helpers": "^0.8.8",
|
"@wrnexus/helpers": "^0.8.8",
|
||||||
"@wrnexus/i18n": "^0.8.10",
|
"@wrnexus/i18n": "^0.8.11",
|
||||||
"@wrnexus/image": "^0.8.9",
|
"@wrnexus/image": "^0.8.10",
|
||||||
"@wrnexus/jwt": "^0.8.8",
|
"@wrnexus/jwt": "^0.8.9",
|
||||||
"@wrnexus/observability": "^0.8.8",
|
"@wrnexus/observability": "^0.8.8",
|
||||||
"@wrnexus/realtime": "^0.8.9",
|
"@wrnexus/realtime": "^0.8.10",
|
||||||
"@wrnexus/security": "^0.8.8",
|
"@wrnexus/security": "^0.8.8",
|
||||||
"@wrnexus/store": "^0.8.8",
|
"@wrnexus/store": "^0.8.8",
|
||||||
"@wrnexus/styles": "^0.8.15",
|
"@wrnexus/styles": "^0.8.15",
|
||||||
"@wrnexus/tracking": "^0.8.8",
|
"@wrnexus/tracking": "^0.8.8",
|
||||||
"@wrnexus/ui": "^0.8.19",
|
"@wrnexus/ui": "^0.8.19",
|
||||||
"@wrnexus/uploader": "^0.8.9",
|
"@wrnexus/uploader": "^0.8.10",
|
||||||
"@wrnexus/validation": "^0.8.9",
|
"@wrnexus/validation": "^0.8.10",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -195,7 +214,7 @@
|
|||||||
"@iconify/tailwind4": "^1.2.3",
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
"@tailwindcss/cli": "^4.3.3",
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@wrnexus/cli": "^0.8.35",
|
"@wrnexus/cli": "^0.8.36",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
@@ -216,7 +235,7 @@
|
|||||||
},
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.12",
|
"version": "0.8.18",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
@@ -238,10 +257,11 @@
|
|||||||
},
|
},
|
||||||
"packages/authz": {
|
"packages/authz": {
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.8.9",
|
"version": "0.8.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/db": "workspace:*",
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/plugin": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/benchmark": {
|
"packages/benchmark": {
|
||||||
@@ -257,7 +277,7 @@
|
|||||||
},
|
},
|
||||||
"packages/captcha": {
|
"packages/captcha": {
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.8.11",
|
"version": "0.8.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -272,7 +292,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.41",
|
"version": "0.8.64",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus": "src/index.ts",
|
"wrnexus": "src/index.ts",
|
||||||
},
|
},
|
||||||
@@ -300,7 +320,7 @@
|
|||||||
},
|
},
|
||||||
"packages/compiler": {
|
"packages/compiler": {
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.8.11",
|
"version": "0.8.21",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/csr": "workspace:*",
|
"@wrnexus/csr": "workspace:*",
|
||||||
"@wrnexus/store": "workspace:*",
|
"@wrnexus/store": "workspace:*",
|
||||||
@@ -318,18 +338,18 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.8.9",
|
"version": "0.8.12",
|
||||||
},
|
},
|
||||||
"packages/csr": {
|
"packages/csr": {
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.8.21",
|
"version": "0.8.34",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/db": {
|
"packages/db": {
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.8.15",
|
"version": "0.8.20",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -337,7 +357,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-server": {
|
"packages/dev-server": {
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.37",
|
"version": "0.8.56",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/cache": "workspace:*",
|
"@wrnexus/cache": "workspace:*",
|
||||||
@@ -351,6 +371,8 @@
|
|||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
"@wrnexus/pubsub": "workspace:*",
|
"@wrnexus/pubsub": "workspace:*",
|
||||||
"@wrnexus/pwa": "workspace:*",
|
"@wrnexus/pwa": "workspace:*",
|
||||||
|
"@wrnexus/queue": "workspace:*",
|
||||||
|
"@wrnexus/react": "workspace:*",
|
||||||
"@wrnexus/router": "workspace:*",
|
"@wrnexus/router": "workspace:*",
|
||||||
"@wrnexus/rpc": "workspace:*",
|
"@wrnexus/rpc": "workspace:*",
|
||||||
"@wrnexus/security": "workspace:*",
|
"@wrnexus/security": "workspace:*",
|
||||||
@@ -364,7 +386,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-toolbar": {
|
"packages/dev-toolbar": {
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.8.12",
|
"version": "0.8.16",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -372,9 +394,10 @@
|
|||||||
},
|
},
|
||||||
"packages/encryption": {
|
"packages/encryption": {
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.8.9",
|
"version": "0.8.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
@@ -390,14 +413,16 @@
|
|||||||
},
|
},
|
||||||
"packages/helpers": {
|
"packages/helpers": {
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/i18n": {
|
"packages/i18n": {
|
||||||
"name": "@wrnexus/i18n",
|
"name": "@wrnexus/i18n",
|
||||||
"version": "0.8.11",
|
"version": "0.8.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -451,13 +476,22 @@
|
|||||||
},
|
},
|
||||||
"packages/language-server": {
|
"packages/language-server": {
|
||||||
"name": "@wrnexus/language-server",
|
"name": "@wrnexus/language-server",
|
||||||
"version": "0.8.9",
|
"version": "0.8.12",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus-language-server": "src/server.ts",
|
"wrnexus-language-server": "src/server.ts",
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "workspace:*",
|
"@wrnexus/syntax": "workspace:*",
|
||||||
"@wrnexus/typecheck": "workspace:*",
|
"@wrnexus/typecheck": "workspace:*",
|
||||||
|
"vscode-html-languageservice": "^5.6.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/mail": {
|
||||||
|
"name": "@wrnexus/mail",
|
||||||
|
"version": "0.8.3",
|
||||||
|
"dependencies": {
|
||||||
|
"@wrnexus/encryption": "workspace:*",
|
||||||
|
"@wrnexus/queue": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/mcp": {
|
"packages/mcp": {
|
||||||
@@ -467,6 +501,10 @@
|
|||||||
"wrnexus-mcp": "src/stdio.ts",
|
"wrnexus-mcp": "src/stdio.ts",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/metering": {
|
||||||
|
"name": "@wrnexus/metering",
|
||||||
|
"version": "0.8.3",
|
||||||
|
},
|
||||||
"packages/mobile": {
|
"packages/mobile": {
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.8.8",
|
"version": "0.8.8",
|
||||||
@@ -492,6 +530,14 @@
|
|||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/payment": {
|
||||||
|
"name": "@wrnexus/payment",
|
||||||
|
"version": "0.8.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/plugin": "workspace:*",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/playground": {
|
"packages/playground": {
|
||||||
"name": "@wrnexus/playground",
|
"name": "@wrnexus/playground",
|
||||||
"version": "0.8.8",
|
"version": "0.8.8",
|
||||||
@@ -525,15 +571,16 @@
|
|||||||
},
|
},
|
||||||
"packages/queue": {
|
"packages/queue": {
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.8.8",
|
"version": "0.8.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
"@wrnexus/rpc": "workspace:*",
|
"@wrnexus/rpc": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/react": {
|
"packages/react": {
|
||||||
"name": "@wrnexus/react",
|
"name": "@wrnexus/react",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/store": "workspace:*",
|
"@wrnexus/store": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -552,7 +599,7 @@
|
|||||||
},
|
},
|
||||||
"packages/reactive": {
|
"packages/reactive": {
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
},
|
},
|
||||||
"packages/realtime": {
|
"packages/realtime": {
|
||||||
"name": "@wrnexus/realtime",
|
"name": "@wrnexus/realtime",
|
||||||
@@ -570,7 +617,7 @@
|
|||||||
},
|
},
|
||||||
"packages/router": {
|
"packages/router": {
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.8.9",
|
"version": "0.8.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "workspace:*",
|
"@wrnexus/compiler": "workspace:*",
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
@@ -592,7 +639,7 @@
|
|||||||
},
|
},
|
||||||
"packages/security": {
|
"packages/security": {
|
||||||
"name": "@wrnexus/security",
|
"name": "@wrnexus/security",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -608,11 +655,11 @@
|
|||||||
},
|
},
|
||||||
"packages/store": {
|
"packages/store": {
|
||||||
"name": "@wrnexus/store",
|
"name": "@wrnexus/store",
|
||||||
"version": "0.8.8",
|
"version": "0.8.12",
|
||||||
},
|
},
|
||||||
"packages/styles": {
|
"packages/styles": {
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.8.15",
|
"version": "0.8.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -621,11 +668,11 @@
|
|||||||
},
|
},
|
||||||
"packages/syntax": {
|
"packages/syntax": {
|
||||||
"name": "@wrnexus/syntax",
|
"name": "@wrnexus/syntax",
|
||||||
"version": "0.8.9",
|
"version": "0.8.17",
|
||||||
},
|
},
|
||||||
"packages/test": {
|
"packages/test": {
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.8.9",
|
"version": "0.8.12",
|
||||||
},
|
},
|
||||||
"packages/tracking": {
|
"packages/tracking": {
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
@@ -633,7 +680,7 @@
|
|||||||
},
|
},
|
||||||
"packages/typecheck": {
|
"packages/typecheck": {
|
||||||
"name": "@wrnexus/typecheck",
|
"name": "@wrnexus/typecheck",
|
||||||
"version": "0.8.10",
|
"version": "0.8.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "workspace:*",
|
"@wrnexus/syntax": "workspace:*",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -641,14 +688,14 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.19",
|
"version": "0.8.42",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/uploader": {
|
"packages/uploader": {
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.8.10",
|
"version": "0.8.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -662,7 +709,7 @@
|
|||||||
},
|
},
|
||||||
"packages/validation": {
|
"packages/validation": {
|
||||||
"name": "@wrnexus/validation",
|
"name": "@wrnexus/validation",
|
||||||
"version": "0.8.10",
|
"version": "0.8.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -749,7 +796,7 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
|
||||||
|
|
||||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
@@ -775,7 +822,7 @@
|
|||||||
|
|
||||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||||
|
|
||||||
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.123", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-0CozmpKXEOEEhltrfT2zt+/t1hez67Y+hM2VJWoIcBKdBrb83VYuKf+b5A0QU9HfQm+RNn2GjFWlTOwi+Ss6IA=="],
|
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.125", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-tOCk1QKMtKnCfPAgZRHgjRkQTP7wF5IO+iPKvvp8vxGZYPkSLhx4HTV3Ng0pIZ3wNWrS6kVpHkunJ1dc19L1og=="],
|
||||||
|
|
||||||
"@iconify/tailwind4": ["@iconify/tailwind4@1.2.3", "", { "dependencies": { "@iconify/tools": "^5.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA=="],
|
"@iconify/tailwind4": ["@iconify/tailwind4@1.2.3", "", { "dependencies": { "@iconify/tools": "^5.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA=="],
|
||||||
|
|
||||||
@@ -795,6 +842,8 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||||
|
|
||||||
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
|
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
|
||||||
|
|
||||||
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||||
@@ -825,79 +874,79 @@
|
|||||||
|
|
||||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||||
|
|
||||||
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA=="],
|
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "@peculiar/asn1-x509-attr": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw=="],
|
||||||
|
|
||||||
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg=="],
|
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw=="],
|
||||||
|
|
||||||
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ=="],
|
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ=="],
|
||||||
|
|
||||||
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-rsa": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg=="],
|
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.9.4", "", { "dependencies": { "@peculiar/asn1-cms": "^2.9.4", "@peculiar/asn1-pkcs8": "^2.9.4", "@peculiar/asn1-rsa": "^2.9.4", "@peculiar/asn1-schema": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw=="],
|
||||||
|
|
||||||
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA=="],
|
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ=="],
|
||||||
|
|
||||||
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pfx": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ=="],
|
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.9.4", "", { "dependencies": { "@peculiar/asn1-cms": "^2.9.4", "@peculiar/asn1-pfx": "^2.9.4", "@peculiar/asn1-pkcs8": "^2.9.4", "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "@peculiar/asn1-x509-attr": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ=="],
|
||||||
|
|
||||||
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg=="],
|
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA=="],
|
||||||
|
|
||||||
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="],
|
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.9.4", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg=="],
|
||||||
|
|
||||||
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg=="],
|
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q=="],
|
||||||
|
|
||||||
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA=="],
|
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg=="],
|
||||||
|
|
||||||
"@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="],
|
"@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="],
|
||||||
|
|
||||||
"@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="],
|
"@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.5", "", { "os": "android", "cpu": "arm" }, "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.5", "", { "os": "android", "cpu": "arm64" }, "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw=="],
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw=="],
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.5", "", { "os": "none", "cpu": "arm64" }, "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg=="],
|
||||||
|
|
||||||
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.3", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "enhanced-resolve": "^5.24.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.3" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="],
|
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.3", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "enhanced-resolve": "^5.24.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.3" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="],
|
||||||
|
|
||||||
@@ -929,7 +978,7 @@
|
|||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
"@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
|
||||||
|
|
||||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||||
|
|
||||||
@@ -937,11 +986,11 @@
|
|||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
|
"@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
|
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="],
|
"@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="],
|
||||||
|
|
||||||
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
||||||
|
|
||||||
@@ -967,6 +1016,8 @@
|
|||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="],
|
||||||
|
|
||||||
|
"@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
|
||||||
|
|
||||||
"@wrnexus/ai": ["@wrnexus/ai@workspace:packages/ai"],
|
"@wrnexus/ai": ["@wrnexus/ai@workspace:packages/ai"],
|
||||||
|
|
||||||
"@wrnexus/auth": ["@wrnexus/auth@workspace:packages/auth"],
|
"@wrnexus/auth": ["@wrnexus/auth@workspace:packages/auth"],
|
||||||
@@ -1011,10 +1062,14 @@
|
|||||||
|
|
||||||
"@wrnexus/language-server": ["@wrnexus/language-server@workspace:packages/language-server"],
|
"@wrnexus/language-server": ["@wrnexus/language-server@workspace:packages/language-server"],
|
||||||
|
|
||||||
|
"@wrnexus/mail": ["@wrnexus/mail@workspace:packages/mail"],
|
||||||
|
|
||||||
"@wrnexus/managed-captcha-service": ["@wrnexus/managed-captcha-service@workspace:services/managed-captcha"],
|
"@wrnexus/managed-captcha-service": ["@wrnexus/managed-captcha-service@workspace:services/managed-captcha"],
|
||||||
|
|
||||||
"@wrnexus/mcp": ["@wrnexus/mcp@workspace:packages/mcp"],
|
"@wrnexus/mcp": ["@wrnexus/mcp@workspace:packages/mcp"],
|
||||||
|
|
||||||
|
"@wrnexus/metering": ["@wrnexus/metering@workspace:packages/metering"],
|
||||||
|
|
||||||
"@wrnexus/mobile": ["@wrnexus/mobile@workspace:packages/mobile"],
|
"@wrnexus/mobile": ["@wrnexus/mobile@workspace:packages/mobile"],
|
||||||
|
|
||||||
"@wrnexus/native": ["@wrnexus/native@workspace:packages/native"],
|
"@wrnexus/native": ["@wrnexus/native@workspace:packages/native"],
|
||||||
@@ -1023,6 +1078,8 @@
|
|||||||
|
|
||||||
"@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"],
|
"@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"],
|
||||||
|
|
||||||
|
"@wrnexus/payment": ["@wrnexus/payment@workspace:packages/payment"],
|
||||||
|
|
||||||
"@wrnexus/playground": ["@wrnexus/playground@workspace:packages/playground"],
|
"@wrnexus/playground": ["@wrnexus/playground@workspace:packages/playground"],
|
||||||
|
|
||||||
"@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"],
|
"@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"],
|
||||||
@@ -1065,7 +1122,7 @@
|
|||||||
|
|
||||||
"@wrnexus/validation": ["@wrnexus/validation@workspace:packages/validation"],
|
"@wrnexus/validation": ["@wrnexus/validation@workspace:packages/validation"],
|
||||||
|
|
||||||
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
@@ -1091,7 +1148,7 @@
|
|||||||
|
|
||||||
"buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
|
"buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
"bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
|
||||||
|
|
||||||
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
||||||
|
|
||||||
@@ -1123,7 +1180,7 @@
|
|||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
@@ -1145,7 +1202,7 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@10.8.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ=="],
|
"eslint": ["eslint@10.9.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||||
|
|
||||||
@@ -1181,7 +1238,7 @@
|
|||||||
|
|
||||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
|
|
||||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
"flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
@@ -1189,7 +1246,7 @@
|
|||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"happy-dom": ["happy-dom@20.11.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw=="],
|
"happy-dom": ["happy-dom@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg=="],
|
||||||
|
|
||||||
"i18n-showcase": ["i18n-showcase@workspace:examples/i18n-showcase"],
|
"i18n-showcase": ["i18n-showcase@workspace:examples/i18n-showcase"],
|
||||||
|
|
||||||
@@ -1261,7 +1318,7 @@
|
|||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||||
|
|
||||||
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
||||||
|
|
||||||
@@ -1297,7 +1354,7 @@
|
|||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
||||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||||
|
|
||||||
@@ -1315,7 +1372,7 @@
|
|||||||
|
|
||||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||||
|
|
||||||
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
|
"pvutils": ["pvutils@1.2.0", "", {}, "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg=="],
|
||||||
|
|
||||||
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
|
|
||||||
@@ -1327,9 +1384,9 @@
|
|||||||
|
|
||||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
|
"rollup": ["rollup@4.62.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.5", "@rollup/rollup-android-arm64": "4.62.5", "@rollup/rollup-darwin-arm64": "4.62.5", "@rollup/rollup-darwin-x64": "4.62.5", "@rollup/rollup-freebsd-arm64": "4.62.5", "@rollup/rollup-freebsd-x64": "4.62.5", "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", "@rollup/rollup-linux-arm-musleabihf": "4.62.5", "@rollup/rollup-linux-arm64-gnu": "4.62.5", "@rollup/rollup-linux-arm64-musl": "4.62.5", "@rollup/rollup-linux-loong64-gnu": "4.62.5", "@rollup/rollup-linux-loong64-musl": "4.62.5", "@rollup/rollup-linux-ppc64-gnu": "4.62.5", "@rollup/rollup-linux-ppc64-musl": "4.62.5", "@rollup/rollup-linux-riscv64-gnu": "4.62.5", "@rollup/rollup-linux-riscv64-musl": "4.62.5", "@rollup/rollup-linux-s390x-gnu": "4.62.5", "@rollup/rollup-linux-x64-gnu": "4.62.5", "@rollup/rollup-linux-x64-musl": "4.62.5", "@rollup/rollup-openbsd-x64": "4.62.5", "@rollup/rollup-openharmony-arm64": "4.62.5", "@rollup/rollup-win32-arm64-msvc": "4.62.5", "@rollup/rollup-win32-ia32-msvc": "4.62.5", "@rollup/rollup-win32-x64-gnu": "4.62.5", "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw=="],
|
||||||
|
|
||||||
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
@@ -1387,6 +1444,14 @@
|
|||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"vscode-html-languageservice": ["vscode-html-languageservice@5.6.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
|
||||||
|
|
||||||
|
"vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="],
|
||||||
|
|
||||||
|
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||||
|
|
||||||
"web": ["web@workspace:examples/inter-app-api-showcase/apps/web"],
|
"web": ["web@workspace:examples/inter-app-api-showcase/apps/web"],
|
||||||
|
|
||||||
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
|
||||||
@@ -1395,27 +1460,29 @@
|
|||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
"wrnexus-crm-example": ["wrnexus-crm-example@workspace:examples/crm-app"],
|
||||||
|
|
||||||
|
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"@antfu/install-pkg/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
"@antfu/install-pkg/tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
|
||||||
|
|
||||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -1288,8 +1288,6 @@ wrnexus help | --help | -h
|
|||||||
import type { AppConfig } from "@wrnexus/styles";
|
import type { AppConfig } from "@wrnexus/styles";
|
||||||
|
|
||||||
const config: AppConfig = {
|
const config: AppConfig = {
|
||||||
compatibilityDate: "2026-08-02",
|
|
||||||
frameworkBehaviour: 1,
|
|
||||||
head: [ '<link rel="stylesheet" href="…">' ], // string | string[] → appended to every <head>
|
head: [ '<link rel="stylesheet" href="…">' ], // string | string[] → appended to every <head>
|
||||||
|
|
||||||
seo: { // SeoConfig (global defaults, merged per page)
|
seo: { // SeoConfig (global defaults, merged per page)
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ Supported view features include:
|
|||||||
- `{#each items as item, index key item.id}`, optional keys, and optional `{:empty}` branches
|
- `{#each items as item, index key item.id}`, optional keys, and optional `{:empty}` branches
|
||||||
- comments and scoped styles
|
- comments and scoped styles
|
||||||
|
|
||||||
|
`{#if}` and `{#each}` are rendered on the server for the initial response and
|
||||||
|
remain reactive after hydration. Browser state changes switch conditional
|
||||||
|
branches and rerender loop rows, including the `{:empty}` branch.
|
||||||
|
|
||||||
Output is escaped by default. Explicit raw HTML APIs must be treated as security
|
Output is escaped by default. Explicit raw HTML APIs must be treated as security
|
||||||
boundaries.
|
boundaries.
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
|||||||
|
# Application productivity kit
|
||||||
|
|
||||||
|
WrNexus applications should declare infrastructure once and keep only domain decisions locally.
|
||||||
|
|
||||||
|
## Included foundations
|
||||||
|
|
||||||
|
1. HTTP routes: `defineApiRoute`, `authorized`, `requireUser`, `requirePermission`, `requireParam`, `requireJson`, and `json`.
|
||||||
|
2. CRUD resources: `defineResource` generates owned list/create/get/update/delete handlers.
|
||||||
|
3. Seed data: `addSeedData`, `upsertSeedData`, `seedIfMissing`, `defineSeed`, `removeSeedData`, and `runSeedQuery`.
|
||||||
|
4. Transactional delivery: `withOutbox`, `drainOutbox`, and `installOutboxSchema`.
|
||||||
|
5. Job state transitions: `defineJobStateMachine`.
|
||||||
|
6. SQLite/PostgreSQL/MySQL queue tables: configured storage plus `databaseQueueStore`.
|
||||||
|
7. Queue operations: success/failed hooks, dashboard handler and queue CLI.
|
||||||
|
8. Queue testing: `testQueue` with deterministic `runNext`/`runAll`.
|
||||||
|
9. Workflows and compensation: durable workflows and `defineSaga`.
|
||||||
|
10. Mail: `defineMail`, `defineMailTemplate`, sandbox allowlists and `queuedMail`.
|
||||||
|
11. Encrypted settings: `defineSecretResource`.
|
||||||
|
12. Ledgers: `defineLedger` with atomic debit and audit entries.
|
||||||
|
13. Forms: `useForm` for values, errors, dirty/submitting state, validation and submission.
|
||||||
|
14. Typed HTTP calls: `createApiClient`; generated OpenAPI/SDK clients remain available through `wrnexus api generate`.
|
||||||
|
15. Full-stack testing: `createTestApp`, database rollback/factories, and queue test controls.
|
||||||
|
16. Package migrations: queue/auth/authz packages contribute migrations through the plugin lifecycle.
|
||||||
|
17. Auth/OAuth: auth and authz plugins own routes, middleware, roles and provider lifecycle.
|
||||||
|
18. Code generation: `wrnexus generate resource <name>`.
|
||||||
|
19. Incremental features: `wrnexus add queue|mail|seed|testing|resource`.
|
||||||
|
20. Starter presets: `wrnexus create app --preset=saas|email` and `--features=...`.
|
||||||
|
|
||||||
|
## Recommended new-project flow
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wrnexus create acme --preset=saas
|
||||||
|
cd acme && bun install
|
||||||
|
wrnexus generate resource projects
|
||||||
|
wrnexus add mail
|
||||||
|
wrnexus db migrate
|
||||||
|
wrnexus db seed
|
||||||
|
wrnexus dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Use generated resources for ordinary owned CRUD. Use the outbox for business operations that
|
||||||
|
change data and enqueue work together. Use sagas only when a workflow crosses systems that cannot
|
||||||
|
share a transaction. Application code should retain product-specific policy; framework code owns
|
||||||
|
storage, validation, lifecycle, retries, diagnostics and scaffolding.
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
# `@wrnexus/payment` — proposal
|
||||||
|
|
||||||
|
**Status:** draft for review
|
||||||
|
**Date:** 2026-08-23
|
||||||
|
|
||||||
|
One package that gives an application real payments: gateway configuration, initialisation,
|
||||||
|
capture, refunds, stored methods, an admin surface, and UI components — so a team never writes
|
||||||
|
this again.
|
||||||
|
|
||||||
|
## The one rule that shapes everything
|
||||||
|
|
||||||
|
**The package never touches a raw card number.**
|
||||||
|
|
||||||
|
Every supported gateway offers hosted fields or a hosted checkout that tokenises the card in
|
||||||
|
the customer's browser, against the gateway's domain, before anything reaches your server. That
|
||||||
|
is what keeps an application in PCI **SAQ-A** — roughly a self-assessment questionnaire —
|
||||||
|
instead of SAQ-D, which is an audit programme with a six-figure floor.
|
||||||
|
|
||||||
|
So the package's UI components mount the gateway's own fields. `PayNow` renders a button that
|
||||||
|
opens a gateway session; it never renders an `<input name="cardNumber">`, and the package
|
||||||
|
exposes no API that would accept one. A framework that makes the cheap wrong thing easy will
|
||||||
|
have it done by someone in a hurry.
|
||||||
|
|
||||||
|
## The second rule: the webhook is the truth
|
||||||
|
|
||||||
|
A browser redirect saying "payment succeeded" is a claim from an untrusted client. The
|
||||||
|
authoritative event is the gateway's signed webhook.
|
||||||
|
|
||||||
|
So `checkPayment()` reads local state that webhooks maintain, and the local record is only ever
|
||||||
|
advanced by a verified webhook or a server-side gateway query — never by a client callback. The
|
||||||
|
redirect is a UX affordance: it tells the customer where to look, not the system what happened.
|
||||||
|
|
||||||
|
This is the single most common way a payment integration leaks money, and it is a design
|
||||||
|
decision, not a runtime check.
|
||||||
|
|
||||||
|
## Money model — the same discipline as metering
|
||||||
|
|
||||||
|
Payments are **append-only**, exactly like `@wrnexus/metering`'s ledger, and for the same
|
||||||
|
reason: a balance you cannot explain is a balance nobody trusts.
|
||||||
|
|
||||||
|
| Table | Holds |
|
||||||
|
| ---------------------- | ----------------------------------------------------------------------------------- |
|
||||||
|
| `wrn_payment_intent` | one row per attempt: amount, currency, gateway, status, idempotency key |
|
||||||
|
| `wrn_payment_event` | append-only; every webhook and state transition, with the raw signed payload |
|
||||||
|
| `wrn_payment_refund` | one row per refund attempt, linked to its intent |
|
||||||
|
| `wrn_payment_method` | stored gateway tokens — never card data, never a PAN, at most a brand and last four |
|
||||||
|
| `wrn_payment_customer` | the mapping from your user id to each gateway's customer id |
|
||||||
|
|
||||||
|
A payment's current status is derived from its events, not from a mutable column that a race can
|
||||||
|
clobber. The `status` column on the intent is a cache of that derivation, and the package ships
|
||||||
|
a reconciliation command that recomputes it and reports disagreements.
|
||||||
|
|
||||||
|
**Idempotency is mandatory, not optional.** Every initialise and every refund carries a key; a
|
||||||
|
repeat with the same key returns the original result rather than charging twice. This is where a
|
||||||
|
double-click becomes a double-charge, and it should be impossible to opt out of.
|
||||||
|
|
||||||
|
## Gateways are adapters
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface PaymentGateway<TConfig = unknown> {
|
||||||
|
readonly id: string; // "stripe" | "razorpay" | "paypal" | ...
|
||||||
|
readonly capabilities: GatewayCapabilities;
|
||||||
|
readonly supports: { currencies: string[] | "any"; countries: string[] | "any" };
|
||||||
|
|
||||||
|
createIntent(input: CreateIntentInput): Promise<GatewayIntent>;
|
||||||
|
fetchIntent(intentRef: string): Promise<GatewayIntent>;
|
||||||
|
cancelIntent(intentRef: string, reason: string): Promise<GatewayIntent>;
|
||||||
|
refund(input: RefundInput): Promise<GatewayRefund>;
|
||||||
|
verifyWebhook(req: Request, secret: string): Promise<WebhookEvent>; // signature check
|
||||||
|
|
||||||
|
capture?(intentRef: string, amount?: Money): Promise<GatewayIntent>; // authorize-then-capture
|
||||||
|
listMethods?(customerRef: string): Promise<GatewayMethod[]>;
|
||||||
|
attachMethod?(customerRef: string, token: string): Promise<GatewayMethod>;
|
||||||
|
detachMethod?(methodRef: string): Promise<void>;
|
||||||
|
createCustomer?(input: CustomerInput): Promise<string>;
|
||||||
|
|
||||||
|
readonly clientConfig: (intent: GatewayIntent) => Record<string, string>; // safe to send
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`clientConfig` exists so the boundary is explicit: it returns exactly what the browser may see —
|
||||||
|
a publishable key and a session id, never a secret. Anything not returned by it cannot reach the
|
||||||
|
client, which is a structural guarantee rather than a code-review habit.
|
||||||
|
|
||||||
|
### Capabilities are declared, and the difference is refused loudly
|
||||||
|
|
||||||
|
This is the part that decides whether a multi-gateway abstraction holds. **Gateways are not
|
||||||
|
interchangeable.** Some have no authorise-then-capture. Some cannot do partial refunds. Some
|
||||||
|
have no stored-method vault. Some only settle in one currency.
|
||||||
|
|
||||||
|
An interface that pretends otherwise produces the worst failure mode there is: an application
|
||||||
|
calls `capturePayment()` against a gateway that has no such concept, and gets a confusing
|
||||||
|
gateway error at the moment money should have moved.
|
||||||
|
|
||||||
|
So every adapter declares what it can do, and the package refuses the rest **at call time with a
|
||||||
|
named reason** — and, better, at **build time** where the gateway is statically known:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface GatewayCapabilities {
|
||||||
|
authorizeThenCapture: boolean; // Stripe yes, Razorpay effectively no
|
||||||
|
partialRefund: boolean;
|
||||||
|
multipleRefunds: boolean;
|
||||||
|
storedMethods: boolean;
|
||||||
|
customerVault: boolean;
|
||||||
|
hostedFields: boolean; // inline hosted inputs
|
||||||
|
hostedCheckout: boolean; // full redirect/modal
|
||||||
|
webhookSignature: boolean; // an adapter without this is refused in production
|
||||||
|
payouts: boolean;
|
||||||
|
disputes: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`capabilitiesOf("razorpay").partialRefund` is a real answer an application can branch on, and
|
||||||
|
`RefundButton` reads it to decide whether to offer an amount field or only a full refund. A
|
||||||
|
capability an adapter does not declare is not merely unimplemented — it is refused, with a
|
||||||
|
message naming the gateway and the capability.
|
||||||
|
|
||||||
|
**`webhookSignature: false` is refused outright in production.** An adapter that cannot verify a
|
||||||
|
webhook cannot be trusted to tell you money moved, and the whole design rests on that.
|
||||||
|
|
||||||
|
### The roster
|
||||||
|
|
||||||
|
**Tier 1 — ship first, fully covered by the contract suite:**
|
||||||
|
|
||||||
|
| Adapter | Notes |
|
||||||
|
| ---------- | ---------------------------------------------------------------------------------- |
|
||||||
|
| `sandbox` | behaves like a real gateway, signed webhooks, injectable failure modes, no network |
|
||||||
|
| `stripe` | global; hosted fields, auth-then-capture, vault, partial and multiple refunds |
|
||||||
|
| `razorpay` | India-first; hosted checkout, INR-centric, no true auth-then-capture |
|
||||||
|
| `paypal` | global; hosted checkout, its own order/capture model |
|
||||||
|
|
||||||
|
Two real gateways prove the abstraction; one does not. Stripe and Razorpay are deliberately the
|
||||||
|
first pair because they **differ** — capture model, currency spread, refund semantics — so the
|
||||||
|
capability system is exercised rather than assumed.
|
||||||
|
|
||||||
|
**Tier 2 — same contract, added after the abstraction has survived Tier 1:**
|
||||||
|
`adyen`, `square`, `braintree`, `mollie`, `checkout.com`, `paddle` (merchant-of-record, so tax
|
||||||
|
and invoicing differ meaningfully).
|
||||||
|
|
||||||
|
**Tier 3 — regional, community-shaped:**
|
||||||
|
`payu`, `cashfree`, `phonepe`, `paytm` (India); `paystack`, `flutterwave` (Africa);
|
||||||
|
`midtrans`, `xendit` (South-East Asia); `authorize.net` (US legacy); `mercadopago` (LatAm).
|
||||||
|
|
||||||
|
### Any developer can add one
|
||||||
|
|
||||||
|
The roster is a starting set, not a ceiling. A custom adapter is a first-class citizen:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineGateway } from "@wrnexus/payment";
|
||||||
|
|
||||||
|
export const acme = defineGateway({
|
||||||
|
id: "acme",
|
||||||
|
capabilities: { partialRefund: true, storedMethods: false, webhookSignature: true /* … */ },
|
||||||
|
supports: { currencies: ["INR", "USD"], countries: ["IN"] },
|
||||||
|
async createIntent(input) {
|
||||||
|
/* … */
|
||||||
|
},
|
||||||
|
async verifyWebhook(req, secret) {
|
||||||
|
/* … */
|
||||||
|
},
|
||||||
|
// …
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`defineGateway` validates the shape at registration and runs the **shared contract suite** in
|
||||||
|
tests, so a third-party adapter is held to exactly the standard the built-in ones are. An adapter
|
||||||
|
that passes the suite behaves identically to Stripe's from the application's point of view; one
|
||||||
|
that does not, fails in CI rather than in production.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Config is **per-gateway and typed** — each adapter declares its own shape, so a missing
|
||||||
|
`webhookSecret` is a type error, not a 3am discovery:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// wrnexus.config.ts
|
||||||
|
payment: {
|
||||||
|
default: "stripe",
|
||||||
|
currency: "INR",
|
||||||
|
|
||||||
|
gateways: {
|
||||||
|
stripe: {
|
||||||
|
secretKey: env("STRIPE_SECRET_KEY"),
|
||||||
|
publishableKey: env("STRIPE_PUBLISHABLE_KEY"),
|
||||||
|
webhookSecret: env("STRIPE_WEBHOOK_SECRET"),
|
||||||
|
apiVersion: "2026-03-31",
|
||||||
|
captureMethod: "automatic", // or "manual" for auth-then-capture
|
||||||
|
},
|
||||||
|
razorpay: {
|
||||||
|
keyId: env("RAZORPAY_KEY_ID"),
|
||||||
|
keySecret: env("RAZORPAY_KEY_SECRET"),
|
||||||
|
webhookSecret: env("RAZORPAY_WEBHOOK_SECRET"),
|
||||||
|
theme: { color: "#0e7c86" },
|
||||||
|
},
|
||||||
|
acme: { apiKey: env("ACME_KEY"), webhookSecret: env("ACME_WEBHOOK_SECRET") },
|
||||||
|
},
|
||||||
|
|
||||||
|
// Optional: pick a gateway per payment instead of always using the default.
|
||||||
|
route(intent) {
|
||||||
|
if (intent.currency === "INR") return "razorpay";
|
||||||
|
if (intent.country === "US") return "stripe";
|
||||||
|
return "stripe";
|
||||||
|
},
|
||||||
|
|
||||||
|
sandbox: process.env.NODE_ENV !== "production",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Selection has three levels, most specific winning: an explicit `gateway` on the call, then
|
||||||
|
`route()`, then `default`. A `route()` that returns a gateway which is not configured, or which
|
||||||
|
cannot settle the intent's currency, is a startup error where it can be detected statically and
|
||||||
|
a named refusal where it cannot.
|
||||||
|
|
||||||
|
**Registering a custom adapter is a config line, not a fork:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { acme } from "./payments/acme.ts";
|
||||||
|
payment: { adapters: [acme], default: "acme", /* … */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Refuse to boot on a misconfiguration rather than failing at the first payment.** A live secret
|
||||||
|
key with `sandbox: true`, or a missing webhook secret, is a startup error naming the variable —
|
||||||
|
the same lesson as `APP_ENCRYPTION_KEY` failing as an opaque WebCrypto error until it was made
|
||||||
|
explicit.
|
||||||
|
|
||||||
|
## The helper surface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Lifecycle
|
||||||
|
initializePayment(input): Promise<PaymentIntent> // amount, currency, subject, metadata, idempotencyKey
|
||||||
|
confirmPayment(id): Promise<PaymentIntent> // server-side confirm where the gateway needs it
|
||||||
|
capturePayment(id, amount?): Promise<PaymentIntent>// for auth-then-capture flows
|
||||||
|
cancelPayment(id, reason): Promise<PaymentIntent>
|
||||||
|
checkPayment(id): Promise<PaymentStatus> // derived from events, never from a client claim
|
||||||
|
syncPayment(id): Promise<PaymentIntent> // authoritative re-read from the gateway
|
||||||
|
|
||||||
|
// Refunds
|
||||||
|
refundPayment({ id, amount?, reason, idempotencyKey }): Promise<Refund> // partial by default
|
||||||
|
listRefunds(id): Promise<Refund[]>
|
||||||
|
refundableAmount(id): Promise<Money> // amount minus refunds already settled
|
||||||
|
|
||||||
|
// Stored methods
|
||||||
|
paymentMethods(userId): Promise<PaymentMethod[]>
|
||||||
|
attachPaymentMethod(userId, token): Promise<PaymentMethod>
|
||||||
|
detachPaymentMethod(methodId): Promise<void>
|
||||||
|
setDefaultPaymentMethod(userId, methodId): Promise<void>
|
||||||
|
|
||||||
|
// Records and reporting
|
||||||
|
getPayment(id) / listPayments(filter) // filter by user, status, gateway, date range
|
||||||
|
paymentTotals(filter): Promise<{ captured, refunded, net, byCurrency }>
|
||||||
|
reconcilePayments(range): Promise<Discrepancy[]> // local vs gateway, the operator's safety net
|
||||||
|
|
||||||
|
// Webhooks
|
||||||
|
paymentWebhookHandler(gatewayId): RouteHandler // signature-verified, idempotent, replay-safe
|
||||||
|
```
|
||||||
|
|
||||||
|
Every function that moves money is **non-throwing and returns a result** with a `fault`
|
||||||
|
discriminator, matching `@wrnexus/metering`: a refusal's reason is safe to show a customer, a
|
||||||
|
fault's reason belongs only in the log. That distinction was learned the hard way — a naive
|
||||||
|
catch once answered "payment required" with a raw SQLite message.
|
||||||
|
|
||||||
|
### `refundableAmount` earns its place
|
||||||
|
|
||||||
|
Partial refunds are where integrations quietly go wrong: two concurrent partial refunds each
|
||||||
|
check "is there enough left?", both see yes, and together exceed the capture. `refundPayment`
|
||||||
|
must enforce the cap with a conditional write — the same shape as metering's reserve — not with
|
||||||
|
a read-then-write.
|
||||||
|
|
||||||
|
## Migrations ship with the package
|
||||||
|
|
||||||
|
`@wrnexus/authz` provisioning nothing is a real cost in this codebase: every application
|
||||||
|
hand-wires `ensureAuthzTables`, and the framework's own example copies DDL by hand and silently
|
||||||
|
drifts. Payment must not repeat that.
|
||||||
|
|
||||||
|
The package is a **plugin**. It contributes its migrations, its webhook route, and its authz
|
||||||
|
permissions (`payment:read`, `payment:refund`, `payment:configure`) on install. An application
|
||||||
|
adds a config block and gets a working, guarded, migrated payment system.
|
||||||
|
|
||||||
|
## UI components
|
||||||
|
|
||||||
|
Every component mounts gateway-hosted fields; none collects card data itself.
|
||||||
|
|
||||||
|
| Component | Does |
|
||||||
|
| ------------------- | -------------------------------------------------------------------------------- |
|
||||||
|
| `PayNow` | the button: opens a gateway session, shows pending/success/failure, emits `paid` |
|
||||||
|
| `PaymentSheet` | hosted fields inline, with the gateway's own validation surfaced |
|
||||||
|
| `PaymentMethodList` | stored methods, set-default, detach — brand and last four only |
|
||||||
|
| `PaymentStatus` | live status for one intent, driven by `checkPayment` |
|
||||||
|
| `RefundButton` | admin-side, requires a reason, shows `refundableAmount` |
|
||||||
|
| `PaymentHistory` | a customer's payments and refunds |
|
||||||
|
| `PaymentSummary` | totals by status and currency for a range |
|
||||||
|
|
||||||
|
`PayNow` needs to survive the customer closing the tab mid-payment, so its resolved state comes
|
||||||
|
from `checkPayment`, not from whether the callback fired.
|
||||||
|
|
||||||
|
**One caveat to state plainly:** these components will hit `GAP-01` today. A `PaymentStatus`
|
||||||
|
that first appears client-side renders as an empty placeholder, and a `PayNow` whose props change
|
||||||
|
after mount will not update. Until the client component runtime lands, these components must be
|
||||||
|
built server-rendered-first with real navigation, exactly as the Sendline admin console was.
|
||||||
|
|
||||||
|
## Admin surface
|
||||||
|
|
||||||
|
A payments console — list and filter, view one payment with its full event timeline, issue a
|
||||||
|
refund with a required reason, inspect webhook deliveries and replay a failed one, and run
|
||||||
|
reconciliation. Every privileged action writes an audit entry, so the package should either take
|
||||||
|
a dependency on an audit interface or define one.
|
||||||
|
|
||||||
|
## Verification — the standard this project now holds
|
||||||
|
|
||||||
|
Unit and integration tests are necessary and insufficient. The package is done when:
|
||||||
|
|
||||||
|
1. Every gateway adapter — built-in **and** third-party — passes one shared contract suite, so
|
||||||
|
behaviour cannot drift per gateway. An adapter declaring a capability it does not honour fails
|
||||||
|
the suite; an adapter honouring one it does not declare fails too, because a silent extra is
|
||||||
|
how an application comes to depend on something the next gateway lacks.
|
||||||
|
2. The sandbox adapter can drive a full lifecycle offline: initialise, webhook, capture, partial
|
||||||
|
refund, over-refund refused, reconciliation clean.
|
||||||
|
3. **A real payment is driven end to end in a browser against a gateway's test mode**, and the
|
||||||
|
money is confirmed in the gateway's own dashboard — not in our database. Phase 3 proved that a
|
||||||
|
row saying `sent` is not the same as an email arriving; a row saying `captured` is not the same
|
||||||
|
as money moving.
|
||||||
|
4. A replayed webhook, a duplicated initialise, and a double-clicked refund each change the
|
||||||
|
ledger exactly once.
|
||||||
|
5. Reconciliation over a deliberately corrupted local row reports the discrepancy rather than
|
||||||
|
hiding it.
|
||||||
|
|
||||||
|
## What this package deliberately does not do
|
||||||
|
|
||||||
|
- **No card data, ever.** No PAN, no CVV, no expiry, in any API, table, log or component.
|
||||||
|
- **No invented gateway.** The sandbox adapter is clearly a sandbox and says so in its UI.
|
||||||
|
- **No subscription billing in v1.** Recurring is a genuinely separate problem — plans, proration,
|
||||||
|
dunning, retries — and bolting it on would compromise both.
|
||||||
|
- **No currency conversion.** Store and settle in the currency charged; a converted number in a
|
||||||
|
ledger is a number nobody can reconcile.
|
||||||
|
- **No silent capture of an expired authorisation.** It fails loudly.
|
||||||
|
|
||||||
|
## Build order
|
||||||
|
|
||||||
|
1. Core: schema, append-only events, status derivation, idempotency, non-throwing results
|
||||||
|
2. The capability system, `defineGateway`, and the shared adapter contract suite
|
||||||
|
3. The sandbox adapter — first consumer of the contract suite, and the thing every later
|
||||||
|
adapter is checked against
|
||||||
|
4. **Stripe, then Razorpay.** Deliberately this pair, because they differ on capture model,
|
||||||
|
currency spread and refund semantics. Building them together is what stops the abstraction
|
||||||
|
quietly becoming "Stripe, with names changed"
|
||||||
|
5. Gateway selection: explicit, `route()`, default — with the misconfiguration errors
|
||||||
|
6. The webhook route: signature verification, replay safety, event storage
|
||||||
|
7. Refunds, including the concurrent partial-refund cap and the `partialRefund: false` path
|
||||||
|
8. Stored methods and customers, behind `storedMethods`
|
||||||
|
9. PayPal — the third gateway, and the real test of whether Tier 2 can be added by someone who
|
||||||
|
did not design the abstraction
|
||||||
|
10. Admin console and reconciliation
|
||||||
|
11. UI components, capability-aware
|
||||||
|
12. Browser verification against a gateway test mode
|
||||||
+249
-14
@@ -91,6 +91,7 @@
|
|||||||
"AuthPluginOptions",
|
"AuthPluginOptions",
|
||||||
"AuthPublicUser",
|
"AuthPublicUser",
|
||||||
"AuthRandom",
|
"AuthRandom",
|
||||||
|
"AuthRequiredError",
|
||||||
"AuthResult",
|
"AuthResult",
|
||||||
"AuthRiskDecision",
|
"AuthRiskDecision",
|
||||||
"AuthRiskLevel",
|
"AuthRiskLevel",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"signUpSchema",
|
"signUpSchema",
|
||||||
"totpUri",
|
"totpUri",
|
||||||
"tryGetDefaultAuthEngine",
|
"tryGetDefaultAuthEngine",
|
||||||
|
"validateProductionAuthConfig",
|
||||||
"verificationRequestSchema",
|
"verificationRequestSchema",
|
||||||
"verificationTokenSchema",
|
"verificationTokenSchema",
|
||||||
"verifyTotp"
|
"verifyTotp"
|
||||||
@@ -229,6 +231,7 @@
|
|||||||
],
|
],
|
||||||
"./middleware": [
|
"./middleware": [
|
||||||
"AUTH_SESSION_KEY",
|
"AUTH_SESSION_KEY",
|
||||||
|
"AuthRequiredError",
|
||||||
"AuthSessionOptions",
|
"AuthSessionOptions",
|
||||||
"RequireAuthOptions",
|
"RequireAuthOptions",
|
||||||
"authSession",
|
"authSession",
|
||||||
@@ -237,7 +240,8 @@
|
|||||||
"getAuthSession",
|
"getAuthSession",
|
||||||
"getAuthUser",
|
"getAuthUser",
|
||||||
"isAuthenticatedContext",
|
"isAuthenticatedContext",
|
||||||
"requireAuth"
|
"requireAuth",
|
||||||
|
"requireAuthUser"
|
||||||
],
|
],
|
||||||
"./passkeys": [
|
"./passkeys": [
|
||||||
"MemoryPasskeyChallengeStore",
|
"MemoryPasskeyChallengeStore",
|
||||||
@@ -252,11 +256,13 @@
|
|||||||
"./plugin": [
|
"./plugin": [
|
||||||
"AuthAuditIssue",
|
"AuthAuditIssue",
|
||||||
"AuthConfig",
|
"AuthConfig",
|
||||||
|
"AuthOAuthProviderConfig",
|
||||||
"AuthPluginOptions",
|
"AuthPluginOptions",
|
||||||
"AuthRoutesConfig",
|
"AuthRoutesConfig",
|
||||||
"authComponentsDir",
|
"authComponentsDir",
|
||||||
"authPlugin",
|
"authPlugin",
|
||||||
"default"
|
"default",
|
||||||
|
"validateProductionAuthConfig"
|
||||||
],
|
],
|
||||||
"./protector": [
|
"./protector": [
|
||||||
"createAuthSecretProtector"
|
"createAuthSecretProtector"
|
||||||
@@ -460,7 +466,9 @@
|
|||||||
"AUTHZ_LOCALS_KEY",
|
"AUTHZ_LOCALS_KEY",
|
||||||
"AttributeMeta",
|
"AttributeMeta",
|
||||||
"AuthorizationDecision",
|
"AuthorizationDecision",
|
||||||
|
"AuthorizationResponses",
|
||||||
"AuthorizeDecisionOptions",
|
"AuthorizeDecisionOptions",
|
||||||
|
"AuthorizedHandlerOptions",
|
||||||
"AuthzAuditEvent",
|
"AuthzAuditEvent",
|
||||||
"AuthzAuditSink",
|
"AuthzAuditSink",
|
||||||
"AuthzCatalog",
|
"AuthzCatalog",
|
||||||
@@ -476,10 +484,12 @@
|
|||||||
"GrantEffect",
|
"GrantEffect",
|
||||||
"GuardOptions",
|
"GuardOptions",
|
||||||
"MemoryAuditSink",
|
"MemoryAuditSink",
|
||||||
|
"OwnedResourceDefinition",
|
||||||
"PermissionMeta",
|
"PermissionMeta",
|
||||||
"PermissionStore",
|
"PermissionStore",
|
||||||
"Policy",
|
"Policy",
|
||||||
"Rbac",
|
"Rbac",
|
||||||
|
"RequestAuthorization",
|
||||||
"Subject",
|
"Subject",
|
||||||
"SubjectAssignments",
|
"SubjectAssignments",
|
||||||
"all",
|
"all",
|
||||||
@@ -497,7 +507,9 @@
|
|||||||
"createAuthzResolver",
|
"createAuthzResolver",
|
||||||
"decideFor",
|
"decideFor",
|
||||||
"decision",
|
"decision",
|
||||||
|
"defineAuthorizedHandler",
|
||||||
"defineAuthz",
|
"defineAuthz",
|
||||||
|
"defineOwnedResource",
|
||||||
"defineRbac",
|
"defineRbac",
|
||||||
"deniedBy",
|
"deniedBy",
|
||||||
"deny",
|
"deny",
|
||||||
@@ -507,6 +519,7 @@
|
|||||||
"filterCan",
|
"filterCan",
|
||||||
"generatePermissionTypes",
|
"generatePermissionTypes",
|
||||||
"getAuthzCatalog",
|
"getAuthzCatalog",
|
||||||
|
"getRequestAuthorization",
|
||||||
"guardPermission",
|
"guardPermission",
|
||||||
"hasAuthzCatalog",
|
"hasAuthzCatalog",
|
||||||
"hasRole",
|
"hasRole",
|
||||||
@@ -525,6 +538,23 @@
|
|||||||
"authzMigrationSql",
|
"authzMigrationSql",
|
||||||
"dbPermissionStore",
|
"dbPermissionStore",
|
||||||
"ensureAuthzTables"
|
"ensureAuthzTables"
|
||||||
|
],
|
||||||
|
"./defaults": [
|
||||||
|
"AuthzIdentityEvent",
|
||||||
|
"assignDefaultAuthzRoles",
|
||||||
|
"setDefaultAuthzRoleStore",
|
||||||
|
"setDefaultAuthzRoles"
|
||||||
|
],
|
||||||
|
"./plugin": [
|
||||||
|
"AuthzConfig",
|
||||||
|
"authzPlugin",
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"./runtime-memory-middleware": [
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"./runtime-middleware": [
|
||||||
|
"default"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/benchmark": {
|
"@wrnexus/benchmark": {
|
||||||
@@ -938,6 +968,7 @@
|
|||||||
},
|
},
|
||||||
"@wrnexus/cli": {
|
"@wrnexus/cli": {
|
||||||
".": [],
|
".": [],
|
||||||
|
"./test-setup": [],
|
||||||
"./workspace": [
|
"./workspace": [
|
||||||
"ResolvedWorkspaceApp",
|
"ResolvedWorkspaceApp",
|
||||||
"ResolvedWorkspaceConfig",
|
"ResolvedWorkspaceConfig",
|
||||||
@@ -1040,7 +1071,8 @@
|
|||||||
"rpcManifest",
|
"rpcManifest",
|
||||||
"runtimeCapabilities",
|
"runtimeCapabilities",
|
||||||
"runtimeTypeOf",
|
"runtimeTypeOf",
|
||||||
"serializeIslandProps"
|
"serializeIslandProps",
|
||||||
|
"stripBrowserTypes"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/content": {
|
"@wrnexus/content": {
|
||||||
@@ -1083,6 +1115,7 @@
|
|||||||
"BackoffStrategy",
|
"BackoffStrategy",
|
||||||
"Bucket",
|
"Bucket",
|
||||||
"BudgetViolation",
|
"BudgetViolation",
|
||||||
|
"BuiltApiRequest",
|
||||||
"Bulkhead",
|
"Bulkhead",
|
||||||
"BulkheadOptions",
|
"BulkheadOptions",
|
||||||
"CSRF_COOKIE",
|
"CSRF_COOKIE",
|
||||||
@@ -1135,6 +1168,8 @@
|
|||||||
"POSTGRES_TENANT_DIRECTORY_SCHEMA",
|
"POSTGRES_TENANT_DIRECTORY_SCHEMA",
|
||||||
"PageComponent",
|
"PageComponent",
|
||||||
"PageMeta",
|
"PageMeta",
|
||||||
|
"PaginationInput",
|
||||||
|
"PaginationOptions",
|
||||||
"PerformanceBudgets",
|
"PerformanceBudgets",
|
||||||
"PerformanceMeasurement",
|
"PerformanceMeasurement",
|
||||||
"PermissionsPolicyConfig",
|
"PermissionsPolicyConfig",
|
||||||
@@ -1196,6 +1231,7 @@
|
|||||||
"TenantResource",
|
"TenantResource",
|
||||||
"TenantSqlClient",
|
"TenantSqlClient",
|
||||||
"Tracer",
|
"Tracer",
|
||||||
|
"TransactionCookie",
|
||||||
"TrustedTypesConfig",
|
"TrustedTypesConfig",
|
||||||
"UploadError",
|
"UploadError",
|
||||||
"UploadInspectionResult",
|
"UploadInspectionResult",
|
||||||
@@ -1203,6 +1239,7 @@
|
|||||||
"UploadScanner",
|
"UploadScanner",
|
||||||
"assertTenantAccess",
|
"assertTenantAccess",
|
||||||
"bridgeRealtime",
|
"bridgeRealtime",
|
||||||
|
"buildApiRequest",
|
||||||
"cacheControl",
|
"cacheControl",
|
||||||
"checkPerformanceBudgets",
|
"checkPerformanceBudgets",
|
||||||
"collectUploads",
|
"collectUploads",
|
||||||
@@ -1228,6 +1265,7 @@
|
|||||||
"escapeHtml",
|
"escapeHtml",
|
||||||
"etag",
|
"etag",
|
||||||
"executionContextFromHttp",
|
"executionContextFromHttp",
|
||||||
|
"getRequestContext",
|
||||||
"getUser",
|
"getUser",
|
||||||
"hashPassword",
|
"hashPassword",
|
||||||
"isRoomDefinition",
|
"isRoomDefinition",
|
||||||
@@ -1259,9 +1297,11 @@
|
|||||||
"requestId",
|
"requestId",
|
||||||
"requestLogger",
|
"requestLogger",
|
||||||
"requireAuth",
|
"requireAuth",
|
||||||
|
"requireRequestContext",
|
||||||
"requireTenant",
|
"requireTenant",
|
||||||
"resilientCall",
|
"resilientCall",
|
||||||
"resolveRequestUrl",
|
"resolveRequestUrl",
|
||||||
|
"runWithRequestContext",
|
||||||
"sanitizeFilename",
|
"sanitizeFilename",
|
||||||
"saveUpload",
|
"saveUpload",
|
||||||
"saveUploadSecure",
|
"saveUploadSecure",
|
||||||
@@ -1313,6 +1353,8 @@
|
|||||||
"ActionClientError",
|
"ActionClientError",
|
||||||
"ActionClientOptions",
|
"ActionClientOptions",
|
||||||
"ActionResult",
|
"ActionResult",
|
||||||
|
"ApiClientOptions",
|
||||||
|
"ApiRequest",
|
||||||
"ClientModuleScope",
|
"ClientModuleScope",
|
||||||
"HydrationScopeApi",
|
"HydrationScopeApi",
|
||||||
"NAV_RUNTIME",
|
"NAV_RUNTIME",
|
||||||
@@ -1326,6 +1368,7 @@
|
|||||||
"callServerFunction",
|
"callServerFunction",
|
||||||
"collectRefs",
|
"collectRefs",
|
||||||
"createActionClient",
|
"createActionClient",
|
||||||
|
"createApiClient",
|
||||||
"createOutputProxy",
|
"createOutputProxy",
|
||||||
"createServerProxy",
|
"createServerProxy",
|
||||||
"getActionRuntime",
|
"getActionRuntime",
|
||||||
@@ -1336,11 +1379,13 @@
|
|||||||
"invalidateClientModule",
|
"invalidateClientModule",
|
||||||
"invokeOutput",
|
"invokeOutput",
|
||||||
"loadClientFunctions",
|
"loadClientFunctions",
|
||||||
"registerOutputHandler"
|
"registerOutputHandler",
|
||||||
|
"useFetch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/db": {
|
"@wrnexus/db": {
|
||||||
".": [
|
".": [
|
||||||
|
"AddSeedOptions",
|
||||||
"BaseType",
|
"BaseType",
|
||||||
"Column",
|
"Column",
|
||||||
"ColumnDef",
|
"ColumnDef",
|
||||||
@@ -1351,6 +1396,7 @@
|
|||||||
"Dialect",
|
"Dialect",
|
||||||
"Driver",
|
"Driver",
|
||||||
"ExecResult",
|
"ExecResult",
|
||||||
|
"LedgerOptions",
|
||||||
"Migration",
|
"Migration",
|
||||||
"MigrationRunOptions",
|
"MigrationRunOptions",
|
||||||
"MigrationSafetyIssue",
|
"MigrationSafetyIssue",
|
||||||
@@ -1367,13 +1413,20 @@
|
|||||||
"RelationOptions",
|
"RelationOptions",
|
||||||
"Repository",
|
"Repository",
|
||||||
"Row",
|
"Row",
|
||||||
|
"SeedDatabase",
|
||||||
|
"SeedRow",
|
||||||
|
"SeedStep",
|
||||||
|
"SeedUserAccount",
|
||||||
"TxHandle",
|
"TxHandle",
|
||||||
|
"UpsertSeedOptions",
|
||||||
|
"addSeedData",
|
||||||
"analyzeMigrationSafety",
|
"analyzeMigrationSafety",
|
||||||
"analyzeMigrations",
|
"analyzeMigrations",
|
||||||
"appliedMigrations",
|
"appliedMigrations",
|
||||||
"applyMigrations",
|
"applyMigrations",
|
||||||
"batch",
|
"batch",
|
||||||
"closeDatabases",
|
"closeDatabases",
|
||||||
|
"compareAndSet",
|
||||||
"countRows",
|
"countRows",
|
||||||
"createDb",
|
"createDb",
|
||||||
"createRepository",
|
"createRepository",
|
||||||
@@ -1381,6 +1434,8 @@
|
|||||||
"cursorPaginate",
|
"cursorPaginate",
|
||||||
"databaseHealth",
|
"databaseHealth",
|
||||||
"databaseNames",
|
"databaseNames",
|
||||||
|
"defineLedger",
|
||||||
|
"defineSeed",
|
||||||
"exists",
|
"exists",
|
||||||
"firstOrThrow",
|
"firstOrThrow",
|
||||||
"generateQueriesFile",
|
"generateQueriesFile",
|
||||||
@@ -1398,15 +1453,20 @@
|
|||||||
"queryOperation",
|
"queryOperation",
|
||||||
"registerDb",
|
"registerDb",
|
||||||
"registerLazyDb",
|
"registerLazyDb",
|
||||||
|
"removeSeedData",
|
||||||
"resetDbPerformanceSnapshot",
|
"resetDbPerformanceSnapshot",
|
||||||
"retryTransaction",
|
"retryTransaction",
|
||||||
"rollback",
|
"rollback",
|
||||||
|
"runSeedQuery",
|
||||||
"scaffoldMigration",
|
"scaffoldMigration",
|
||||||
|
"seedIfMissing",
|
||||||
|
"seedUsers",
|
||||||
"setDb",
|
"setDb",
|
||||||
"softDeleteClause",
|
"softDeleteClause",
|
||||||
"status",
|
"status",
|
||||||
"table",
|
"table",
|
||||||
"tenantScope",
|
"tenantScope",
|
||||||
|
"upsertSeedData",
|
||||||
"v",
|
"v",
|
||||||
"withTransaction"
|
"withTransaction"
|
||||||
],
|
],
|
||||||
@@ -1464,6 +1524,7 @@
|
|||||||
"startServer",
|
"startServer",
|
||||||
"toRequest",
|
"toRequest",
|
||||||
"validateRpcCsrf",
|
"validateRpcCsrf",
|
||||||
|
"withServerFnRequestContext",
|
||||||
"writeResponse"
|
"writeResponse"
|
||||||
],
|
],
|
||||||
"./serve-entry": []
|
"./serve-entry": []
|
||||||
@@ -1606,6 +1667,7 @@
|
|||||||
"EncryptionKey",
|
"EncryptionKey",
|
||||||
"EncryptionKeyring",
|
"EncryptionKeyring",
|
||||||
"ReplayStore",
|
"ReplayStore",
|
||||||
|
"SecretResourceOptions",
|
||||||
"createEncryptedRequest",
|
"createEncryptedRequest",
|
||||||
"createKeyring",
|
"createKeyring",
|
||||||
"createMemoryReplayStore",
|
"createMemoryReplayStore",
|
||||||
@@ -1613,6 +1675,7 @@
|
|||||||
"decryptEncryptedResponse",
|
"decryptEncryptedResponse",
|
||||||
"decryptHttpBody",
|
"decryptHttpBody",
|
||||||
"decryptRequest",
|
"decryptRequest",
|
||||||
|
"defineSecretResource",
|
||||||
"deriveKey",
|
"deriveKey",
|
||||||
"encrypt",
|
"encrypt",
|
||||||
"encryptHttpBody",
|
"encryptHttpBody",
|
||||||
@@ -1646,26 +1709,39 @@
|
|||||||
"@wrnexus/helpers": {
|
"@wrnexus/helpers": {
|
||||||
".": [
|
".": [
|
||||||
"AllowedHosts",
|
"AllowedHosts",
|
||||||
|
"ApiHandler",
|
||||||
|
"HttpError",
|
||||||
"LoginRedirectOptions",
|
"LoginRedirectOptions",
|
||||||
"OriginalRequestOptions",
|
"OriginalRequestOptions",
|
||||||
"RequestContext",
|
"RequestContext",
|
||||||
|
"ResourceDefinition",
|
||||||
|
"ResourceHandlers",
|
||||||
"RetryOptions",
|
"RetryOptions",
|
||||||
"appOrigin",
|
"appOrigin",
|
||||||
"appUrl",
|
"appUrl",
|
||||||
|
"authorized",
|
||||||
"backoffDelay",
|
"backoffDelay",
|
||||||
"clamp",
|
"clamp",
|
||||||
"currentAppName",
|
"currentAppName",
|
||||||
"currentAppOrigin",
|
"currentAppOrigin",
|
||||||
|
"defineApiRoute",
|
||||||
|
"defineResource",
|
||||||
"getOriginalRequestMethod",
|
"getOriginalRequestMethod",
|
||||||
"getOriginalRequestOrigin",
|
"getOriginalRequestOrigin",
|
||||||
"getOriginalRequestPath",
|
"getOriginalRequestPath",
|
||||||
"getOriginalRequestUrl",
|
"getOriginalRequestUrl",
|
||||||
|
"json",
|
||||||
"once",
|
"once",
|
||||||
"redirectToLogin",
|
"redirectToLogin",
|
||||||
|
"requireJson",
|
||||||
|
"requireParam",
|
||||||
|
"requirePermission",
|
||||||
|
"requireUser",
|
||||||
"retry",
|
"retry",
|
||||||
"safeJsonParse",
|
"safeJsonParse",
|
||||||
"sleep",
|
"sleep",
|
||||||
"stableStringify",
|
"stableStringify",
|
||||||
|
"subjectId",
|
||||||
"withTimeout",
|
"withTimeout",
|
||||||
"workspaceAppOrigins",
|
"workspaceAppOrigins",
|
||||||
"workspaceRootDomain"
|
"workspaceRootDomain"
|
||||||
@@ -1674,6 +1750,7 @@
|
|||||||
"@wrnexus/i18n": {
|
"@wrnexus/i18n": {
|
||||||
".": [
|
".": [
|
||||||
"ExtractedTranslationKey",
|
"ExtractedTranslationKey",
|
||||||
|
"I18N_DATA_ATTRIBUTE",
|
||||||
"I18N_JS_HREF",
|
"I18N_JS_HREF",
|
||||||
"I18N_RUNTIME",
|
"I18N_RUNTIME",
|
||||||
"I18nConfig",
|
"I18nConfig",
|
||||||
@@ -1710,6 +1787,7 @@
|
|||||||
"plural",
|
"plural",
|
||||||
"pseudoLocalize",
|
"pseudoLocalize",
|
||||||
"renderI18nData",
|
"renderI18nData",
|
||||||
|
"renderI18nDataTag",
|
||||||
"resolveI18n",
|
"resolveI18n",
|
||||||
"resolveLang",
|
"resolveLang",
|
||||||
"translateHtml",
|
"translateHtml",
|
||||||
@@ -1831,7 +1909,7 @@
|
|||||||
"Position",
|
"Position",
|
||||||
"Range",
|
"Range",
|
||||||
"TextDocument",
|
"TextDocument",
|
||||||
"WRN_COMPLETIONS",
|
"WRN_KEYWORDS",
|
||||||
"WorkspaceCompletionItem",
|
"WorkspaceCompletionItem",
|
||||||
"clearWorkspaceIndexCache",
|
"clearWorkspaceIndexCache",
|
||||||
"completionItems",
|
"completionItems",
|
||||||
@@ -1852,7 +1930,31 @@
|
|||||||
"workspaceCompletionItems",
|
"workspaceCompletionItems",
|
||||||
"workspaceSymbolLocations"
|
"workspaceSymbolLocations"
|
||||||
],
|
],
|
||||||
"./server": []
|
"./server": [
|
||||||
|
"ApiCallCompletionItem",
|
||||||
|
"apiCallCompletions",
|
||||||
|
"apiCallHover",
|
||||||
|
"isApiReferenceAt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@wrnexus/mail": {
|
||||||
|
".": [
|
||||||
|
"MailDriver",
|
||||||
|
"MailMessage",
|
||||||
|
"MailOptions",
|
||||||
|
"MailSandboxDecision",
|
||||||
|
"MailTemplate",
|
||||||
|
"SealedMailCredentialsOptions",
|
||||||
|
"SealedMailDriverOptions",
|
||||||
|
"defineMail",
|
||||||
|
"defineMailTemplate",
|
||||||
|
"defineSealedMailCredentials",
|
||||||
|
"interpolateTemplate",
|
||||||
|
"mailSandboxDecision",
|
||||||
|
"queuedMail",
|
||||||
|
"sealedMailDriver",
|
||||||
|
"storedMailTemplate"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/mcp": {
|
"@wrnexus/mcp": {
|
||||||
".": [
|
".": [
|
||||||
@@ -1865,6 +1967,20 @@
|
|||||||
"runMcpStdio"
|
"runMcpStdio"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@wrnexus/metering": {
|
||||||
|
".": [
|
||||||
|
"EntitlementPlan",
|
||||||
|
"EntitlementsOptions",
|
||||||
|
"MeterKind",
|
||||||
|
"MeterOptions",
|
||||||
|
"MeterPack",
|
||||||
|
"MeterResult",
|
||||||
|
"MeterStore",
|
||||||
|
"defineEntitlements",
|
||||||
|
"defineMeter",
|
||||||
|
"definePacks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@wrnexus/mobile": {
|
"@wrnexus/mobile": {
|
||||||
".": [
|
".": [
|
||||||
"CapacitorBridge",
|
"CapacitorBridge",
|
||||||
@@ -2065,6 +2181,70 @@
|
|||||||
"traceMiddleware"
|
"traceMiddleware"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@wrnexus/payment": {
|
||||||
|
".": [
|
||||||
|
"CreateIntentInput",
|
||||||
|
"CustomerInput",
|
||||||
|
"GatewayCapabilities",
|
||||||
|
"GatewayContractFixture",
|
||||||
|
"GatewayContractReport",
|
||||||
|
"GatewayIntent",
|
||||||
|
"GatewayMethod",
|
||||||
|
"GatewayRefund",
|
||||||
|
"GatewaySupport",
|
||||||
|
"InitializePaymentInput",
|
||||||
|
"Money",
|
||||||
|
"PayPalConfig",
|
||||||
|
"PaymentEvent",
|
||||||
|
"PaymentGateway",
|
||||||
|
"PaymentIntent",
|
||||||
|
"PaymentMethod",
|
||||||
|
"PaymentOptions",
|
||||||
|
"PaymentRefund",
|
||||||
|
"PaymentResult",
|
||||||
|
"PaymentService",
|
||||||
|
"PaymentStatus",
|
||||||
|
"PaymentStore",
|
||||||
|
"RazorpayConfig",
|
||||||
|
"RefundInput",
|
||||||
|
"SandboxOptions",
|
||||||
|
"StripeConfig",
|
||||||
|
"WebhookEvent",
|
||||||
|
"configurePayment",
|
||||||
|
"databasePaymentStore",
|
||||||
|
"defineGateway",
|
||||||
|
"definePayment",
|
||||||
|
"derivePaymentStatus",
|
||||||
|
"exerciseGatewayContract",
|
||||||
|
"getPaymentService",
|
||||||
|
"memoryPaymentStore",
|
||||||
|
"paymentPlugin",
|
||||||
|
"paypalGateway",
|
||||||
|
"razorpayGateway",
|
||||||
|
"sandboxGateway",
|
||||||
|
"signSandboxWebhook",
|
||||||
|
"stripeGateway",
|
||||||
|
"verifyGatewayContract"
|
||||||
|
],
|
||||||
|
"./adapters": [
|
||||||
|
"PayPalConfig",
|
||||||
|
"RazorpayConfig",
|
||||||
|
"SandboxOptions",
|
||||||
|
"StripeConfig",
|
||||||
|
"paypalGateway",
|
||||||
|
"razorpayGateway",
|
||||||
|
"sandboxGateway",
|
||||||
|
"signSandboxWebhook",
|
||||||
|
"stripeGateway"
|
||||||
|
],
|
||||||
|
"./plugin": [
|
||||||
|
"default",
|
||||||
|
"paymentPlugin"
|
||||||
|
],
|
||||||
|
"./runtime-webhook": [
|
||||||
|
"POST"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@wrnexus/playground": {
|
"@wrnexus/playground": {
|
||||||
".": [
|
".": [
|
||||||
"PlaygroundCompilation",
|
"PlaygroundCompilation",
|
||||||
@@ -2229,23 +2409,44 @@
|
|||||||
"@wrnexus/queue": {
|
"@wrnexus/queue": {
|
||||||
".": [
|
".": [
|
||||||
"AddOptions",
|
"AddOptions",
|
||||||
|
"DefineQueueOptions",
|
||||||
|
"DefinedJob",
|
||||||
|
"DefinedJobOptions",
|
||||||
|
"DefinedJobs",
|
||||||
|
"DefinedQueue",
|
||||||
"DurableQueue",
|
"DurableQueue",
|
||||||
"DurableQueueOptions",
|
"DurableQueueOptions",
|
||||||
"Job",
|
"Job",
|
||||||
"JobContext",
|
"JobContext",
|
||||||
"JobDefinition",
|
"JobDefinition",
|
||||||
"JobHandler",
|
"JobHandler",
|
||||||
|
"JobStateMachineOptions",
|
||||||
|
"JobStatus",
|
||||||
|
"OutboxEntry",
|
||||||
|
"OutboxWriter",
|
||||||
"POSTGRES_QUEUE_SCHEMA",
|
"POSTGRES_QUEUE_SCHEMA",
|
||||||
"Queue",
|
"Queue",
|
||||||
|
"QueueAdminOptions",
|
||||||
"QueueDashboardSnapshot",
|
"QueueDashboardSnapshot",
|
||||||
|
"QueueEvent",
|
||||||
|
"QueueEventType",
|
||||||
|
"QueueHealth",
|
||||||
|
"QueueJobDefinitions",
|
||||||
|
"QueueJobRecord",
|
||||||
|
"QueueJobState",
|
||||||
"QueueOptions",
|
"QueueOptions",
|
||||||
"QueueScheduler",
|
"QueueScheduler",
|
||||||
|
"QueueStorage",
|
||||||
|
"QueueStorageConfig",
|
||||||
"QueueStore",
|
"QueueStore",
|
||||||
"RedisQueueClient",
|
"RedisQueueClient",
|
||||||
|
"SagaStep",
|
||||||
"ScheduledJob",
|
"ScheduledJob",
|
||||||
"SqlQueueClient",
|
"SqlQueueClient",
|
||||||
|
"SqliteQueueClient",
|
||||||
"SubjectJob",
|
"SubjectJob",
|
||||||
"SubjectQueue",
|
"SubjectQueue",
|
||||||
|
"WorkerDefinition",
|
||||||
"WorkflowDefinition",
|
"WorkflowDefinition",
|
||||||
"WorkflowEngine",
|
"WorkflowEngine",
|
||||||
"WorkflowRunContext",
|
"WorkflowRunContext",
|
||||||
@@ -2254,22 +2455,46 @@
|
|||||||
"WorkflowStep",
|
"WorkflowStep",
|
||||||
"WorkflowStore",
|
"WorkflowStore",
|
||||||
"addBatch",
|
"addBatch",
|
||||||
|
"configureQueueStorage",
|
||||||
|
"configuredQueueStore",
|
||||||
"createDurableQueue",
|
"createDurableQueue",
|
||||||
"createQueue",
|
"createQueue",
|
||||||
|
"createQueueAdminHandler",
|
||||||
"createQueueScheduler",
|
"createQueueScheduler",
|
||||||
"createWorkflowEngine",
|
"createWorkflowEngine",
|
||||||
"cronToInterval",
|
"cronToInterval",
|
||||||
|
"databaseQueueStore",
|
||||||
"defineDurableWorkflow",
|
"defineDurableWorkflow",
|
||||||
"defineJob",
|
"defineJob",
|
||||||
|
"defineJobStateMachine",
|
||||||
|
"defineQueue",
|
||||||
|
"defineSaga",
|
||||||
|
"defineWorker",
|
||||||
"defineWorkflow",
|
"defineWorkflow",
|
||||||
|
"drainOutbox",
|
||||||
|
"installDatabaseQueueSchema",
|
||||||
|
"installOutboxSchema",
|
||||||
|
"installSqliteQueueSchema",
|
||||||
"memoryQueueStore",
|
"memoryQueueStore",
|
||||||
"memoryWorkflowStore",
|
"memoryWorkflowStore",
|
||||||
"postgresQueueStore",
|
"postgresQueueStore",
|
||||||
"queueDashboardSnapshot",
|
"queueDashboardSnapshot",
|
||||||
|
"queueStorageConfig",
|
||||||
"redisQueueStore",
|
"redisQueueStore",
|
||||||
"renderQueueDashboard",
|
"renderQueueDashboard",
|
||||||
"runQueueDaemon",
|
"runQueueDaemon",
|
||||||
"subjectQueue"
|
"runWorker",
|
||||||
|
"sqliteQueueSchema",
|
||||||
|
"sqliteQueueStore",
|
||||||
|
"subjectQueue",
|
||||||
|
"testQueue",
|
||||||
|
"withOutbox"
|
||||||
|
],
|
||||||
|
"./sqlite": [
|
||||||
|
"SqliteQueueClient",
|
||||||
|
"installSqliteQueueSchema",
|
||||||
|
"sqliteQueueSchema",
|
||||||
|
"sqliteQueueStore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/react": {
|
"@wrnexus/react": {
|
||||||
@@ -2312,6 +2537,7 @@
|
|||||||
".": [
|
".": [
|
||||||
"AnimationTimeline",
|
"AnimationTimeline",
|
||||||
"Cleanup",
|
"Cleanup",
|
||||||
|
"FormState",
|
||||||
"HistorySignal",
|
"HistorySignal",
|
||||||
"ReactiveContext",
|
"ReactiveContext",
|
||||||
"ReactiveScope",
|
"ReactiveScope",
|
||||||
@@ -2338,6 +2564,7 @@
|
|||||||
"transition",
|
"transition",
|
||||||
"untrack",
|
"untrack",
|
||||||
"urlSignal",
|
"urlSignal",
|
||||||
|
"useForm",
|
||||||
"watch"
|
"watch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2625,15 +2852,12 @@
|
|||||||
".": [
|
".": [
|
||||||
"ACCENT_COOKIE",
|
"ACCENT_COOKIE",
|
||||||
"AppConfig",
|
"AppConfig",
|
||||||
|
"AppConfigInput",
|
||||||
"BrowserCookieApi",
|
"BrowserCookieApi",
|
||||||
"BrowserCookieOptions",
|
"BrowserCookieOptions",
|
||||||
"BrowserCookiePreference",
|
"BrowserCookiePreference",
|
||||||
"BrowserCookiesConfig",
|
"BrowserCookiesConfig",
|
||||||
"BuildConfig",
|
"BuildConfig",
|
||||||
"CURRENT_COMPATIBILITY_DATE",
|
|
||||||
"CURRENT_FRAMEWORK_BEHAVIOUR",
|
|
||||||
"CompatibilityPolicy",
|
|
||||||
"CompatibilityReport",
|
|
||||||
"ConfigIssue",
|
"ConfigIssue",
|
||||||
"ContrastResult",
|
"ContrastResult",
|
||||||
"CssPerformanceAuditIssue",
|
"CssPerformanceAuditIssue",
|
||||||
@@ -2653,6 +2877,7 @@
|
|||||||
"ObservabilityConfig",
|
"ObservabilityConfig",
|
||||||
"PerformanceConfig",
|
"PerformanceConfig",
|
||||||
"PwaConfig",
|
"PwaConfig",
|
||||||
|
"ResolvedAppConfig",
|
||||||
"ResolvedConfigLayers",
|
"ResolvedConfigLayers",
|
||||||
"ResolvedTheme",
|
"ResolvedTheme",
|
||||||
"StyleProcessContext",
|
"StyleProcessContext",
|
||||||
@@ -2684,7 +2909,6 @@
|
|||||||
"findStyleEntry",
|
"findStyleEntry",
|
||||||
"fontCspSources",
|
"fontCspSources",
|
||||||
"headToString",
|
"headToString",
|
||||||
"isCompatibilityDate",
|
|
||||||
"loadAppConfig",
|
"loadAppConfig",
|
||||||
"loadEnv",
|
"loadEnv",
|
||||||
"loadRawConfig",
|
"loadRawConfig",
|
||||||
@@ -2697,7 +2921,6 @@
|
|||||||
"renderThemeRuntime",
|
"renderThemeRuntime",
|
||||||
"resolveAccentName",
|
"resolveAccentName",
|
||||||
"resolveBrowserCookieOptions",
|
"resolveBrowserCookieOptions",
|
||||||
"resolveCompatibility",
|
|
||||||
"resolveConfigLayers",
|
"resolveConfigLayers",
|
||||||
"resolveProfile",
|
"resolveProfile",
|
||||||
"resolveThemeConfig",
|
"resolveThemeConfig",
|
||||||
@@ -2785,6 +3008,7 @@
|
|||||||
"parseStructuredImports",
|
"parseStructuredImports",
|
||||||
"positionAt",
|
"positionAt",
|
||||||
"runtimeTypeOf",
|
"runtimeTypeOf",
|
||||||
|
"skipLiteralOrComment",
|
||||||
"sliceSource",
|
"sliceSource",
|
||||||
"stripRuntimeFunctionModifiers",
|
"stripRuntimeFunctionModifiers",
|
||||||
"supportsSyntaxFeature",
|
"supportsSyntaxFeature",
|
||||||
@@ -2851,7 +3075,10 @@
|
|||||||
"LexError",
|
"LexError",
|
||||||
"Lexer",
|
"Lexer",
|
||||||
"Token",
|
"Token",
|
||||||
"TokenType"
|
"TokenType",
|
||||||
|
"isIdentPart",
|
||||||
|
"isIdentStart",
|
||||||
|
"skipLiteralOrComment"
|
||||||
],
|
],
|
||||||
"./types": [
|
"./types": [
|
||||||
"RuntimeType",
|
"RuntimeType",
|
||||||
@@ -2865,10 +3092,14 @@
|
|||||||
".": [
|
".": [
|
||||||
"BrowserArtifactPage",
|
"BrowserArtifactPage",
|
||||||
"Deferred",
|
"Deferred",
|
||||||
|
"DetectMutationsOptions",
|
||||||
|
"FetchMock",
|
||||||
"Harness",
|
"Harness",
|
||||||
"HarnessOptions",
|
"HarnessOptions",
|
||||||
"JsonResponse",
|
"JsonResponse",
|
||||||
"MemoryCookieJar",
|
"MemoryCookieJar",
|
||||||
|
"MutationCase",
|
||||||
|
"MutationReport",
|
||||||
"TestRequestOptions",
|
"TestRequestOptions",
|
||||||
"TransactionalDatabase",
|
"TransactionalDatabase",
|
||||||
"WaitForOptions",
|
"WaitForOptions",
|
||||||
@@ -2880,9 +3111,13 @@
|
|||||||
"captureBrowserArtifacts",
|
"captureBrowserArtifacts",
|
||||||
"createContext",
|
"createContext",
|
||||||
"createFactory",
|
"createFactory",
|
||||||
|
"createFetchMock",
|
||||||
"createHarness",
|
"createHarness",
|
||||||
|
"createTestApp",
|
||||||
|
"createTestContext",
|
||||||
"deferred",
|
"deferred",
|
||||||
"describe",
|
"describe",
|
||||||
|
"detectMutations",
|
||||||
"expect",
|
"expect",
|
||||||
"expectProblem",
|
"expectProblem",
|
||||||
"it",
|
"it",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,400 @@
|
|||||||
|
# Editor Tooling Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** The language server and VS Code extension understand `apis { }`, complete `api.<name>()`, flag the removed constructs, and stop offering syntax the compiler rejects.
|
||||||
|
|
||||||
|
**Architecture:** Four surfaces change independently — the TextMate grammar, the keyword and snippet completions, the server's call completion and hover, and diagnostics for removed constructs. A final task answers by observation whether generated type errors surface inside `.wrn`, which has never been verified.
|
||||||
|
|
||||||
|
**Tech Stack:** Bun, TypeScript, `bun:test`, `node --test`, LSP, TextMate grammars.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-19-editor-tooling-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- `client` is one word with several jobs: `client state { }`, `runtime = "client"`, and the `client function` modifier all survive. **Only the `client { }` / `ssr { }` data-block patterns are removed.** Blanket removal would un-highlight constructs that still exist.
|
||||||
|
- Offering a construct the compiler rejects is worse than offering nothing.
|
||||||
|
- **Nothing may claim inline diagnostics work until someone has seen them work.**
|
||||||
|
- The editor bundles embed the compiler and language server — rebuild with `bun run --cwd editors/vscode build`, or the `check:editor-*` gates fail on a stale bundle.
|
||||||
|
- `bun run format` before every commit; the gate is `bun run check:production`.
|
||||||
|
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Grammar
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `editors/vscode/syntaxes/wrn.tmLanguage.json`
|
||||||
|
- Test: `editors/vscode/test/grammar-apis.test.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `apis` highlights as a block keyword; entries highlight as declarations.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `editors/vscode/test/grammar-apis.test.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const { readFileSync } = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const grammar = readFileSync(join(__dirname, "../syntaxes/wrn.tmLanguage.json"), "utf8");
|
||||||
|
|
||||||
|
test("the grammar knows the apis block", () => {
|
||||||
|
assert.ok(grammar.includes("apis"), "apis should appear as a block keyword");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("client keeps its highlighting where it is still valid", () => {
|
||||||
|
// client state {}, runtime = "client", and client function all survive.
|
||||||
|
// Only the client {} data block was removed.
|
||||||
|
assert.ok(grammar.includes("client"), "client must still be matched");
|
||||||
|
assert.ok(grammar.includes("shared"), "the shared function modifier must still be matched");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `node --test editors/vscode/test/grammar-apis.test.js`
|
||||||
|
Expected: FAIL on the first assertion — `apis` is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `apis`, remove only the data-block patterns**
|
||||||
|
|
||||||
|
Add `apis` to the block-keyword pattern alongside `functions`. Then find the patterns matching `ssr`/`client` as **data blocks** and remove only those. Leave every rule that matches `client` in `client state`, in `runtime` values, and as a function modifier.
|
||||||
|
|
||||||
|
Add a pattern for an entry — `<name> <METHOD> <path>` — so a declaration reads as a declaration.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the test and check by eye**
|
||||||
|
|
||||||
|
Run: `node --test editors/vscode/test/grammar-apis.test.js`
|
||||||
|
Expected: PASS. Then open a `.wrn` file using `apis { }`, `client state { }`, `functions { shared function }`, and `runtime = "client"` in VS Code and confirm each still colours correctly. Record what you saw.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add editors/vscode
|
||||||
|
git commit -m "feat(editor): highlight the apis block"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Keyword and snippet completion
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/language-server/src/index.ts` (`WRN_KEYWORDS`, ~line 31)
|
||||||
|
- Modify: `editors/vscode/src/completion.js` (block snippets)
|
||||||
|
- Test: `packages/language-server/test/apis-completion.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing.
|
||||||
|
- Produces: `apis` is a known keyword; `ssr` / `client` data-block snippets are gone.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/language-server/test/apis-completion.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { WRN_KEYWORDS } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("apis is a known page-level keyword", () => {
|
||||||
|
expect(WRN_KEYWORDS).toContain("apis");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server/test/apis-completion.test.ts`
|
||||||
|
Expected: FAIL — `apis` is missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the keyword and the snippets**
|
||||||
|
|
||||||
|
Add `"apis"` to `WRN_KEYWORDS`. In `editors/vscode/src/completion.js`, add a container snippet and an entry snippet including the `request` / `response` / `error` sections, and remove any `ssr {` / `client {` data-block snippet.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server && bun run --cwd editors/vscode test`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/language-server editors/vscode
|
||||||
|
git commit -m "feat(editor): complete the apis block and drop the removed snippets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: `api.` call completion and hover
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/language-server/src/server.ts` (completion and hover handlers)
|
||||||
|
- Test: `packages/language-server/test/api-call-completion.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `ast.dataApis` entries, which carry `name`, `method`, `path`, and `sections`.
|
||||||
|
- Produces: completion items for `api.` and hover detail for a block name.
|
||||||
|
|
||||||
|
This is where the syntax pays off in the editor: the set of legal calls is knowable, so the editor should know it.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/language-server/test/api-call-completion.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { apiCallCompletions, apiCallHover } from "../src/server.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Search {
|
||||||
|
apis {
|
||||||
|
searchUsers POST /api/users {
|
||||||
|
request { body { name?: string } }
|
||||||
|
response { return data.users }
|
||||||
|
}
|
||||||
|
|
||||||
|
listTeams GET /api/teams {
|
||||||
|
response { return data.teams }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
functions {
|
||||||
|
client async function go(): Promise<void> {
|
||||||
|
await api.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("api. offers every declared block with method and path", () => {
|
||||||
|
const items = apiCallCompletions(SOURCE);
|
||||||
|
const labels = items.map((item) => item.label);
|
||||||
|
|
||||||
|
expect(labels).toContain("searchUsers");
|
||||||
|
expect(labels).toContain("listTeams");
|
||||||
|
|
||||||
|
const search = items.find((item) => item.label === "searchUsers")!;
|
||||||
|
expect(search.detail).toContain("POST");
|
||||||
|
expect(search.detail).toContain("/api/users");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hovering a block name reports its method, path and request fields", () => {
|
||||||
|
const hover = apiCallHover(SOURCE, "searchUsers");
|
||||||
|
|
||||||
|
expect(hover).toContain("POST");
|
||||||
|
expect(hover).toContain("/api/users");
|
||||||
|
expect(hover).toContain("name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a page with no apis block offers nothing", () => {
|
||||||
|
expect(apiCallCompletions(`page P { view { <main>x</main> } }`)).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server/test/api-call-completion.test.ts`
|
||||||
|
Expected: FAIL — the functions do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement and export both functions**
|
||||||
|
|
||||||
|
Add `apiCallCompletions(source: string)` and `apiCallHover(source: string, name: string)` to `packages/language-server/src/server.ts`, parsing with `@wrnexus/syntax` and reading `ast.dataApis`. Wire `apiCallCompletions` into the `textDocument/completion` handler for positions immediately after `api.`, and `apiCallHover` into `textDocument/hover`.
|
||||||
|
|
||||||
|
The parser must tolerate the half-typed `await api.` in the fixture. If it throws, fall back to returning `[]` rather than failing the request — completion fires while the document does not parse, which is the normal case.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/language-server
|
||||||
|
git commit -m "feat(language-server): complete and describe api block calls"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: The `api=` attribute in markup
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/language-server/src/html-service.ts`
|
||||||
|
- Test: `packages/language-server/test/api-attribute.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `apiCallCompletions` from Task 3.
|
||||||
|
- Produces: `api="…"` is not flagged as unknown, and completion inside the quotes offers block names.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/language-server/test/api-attribute.test.ts` asserting that (a) an `api="searchUsers"` attribute produces no unknown-attribute diagnostic, and (b) completion inside the quotes offers `searchUsers`. Reuse the fixture shape from Task 3.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server/test/api-attribute.test.ts`
|
||||||
|
Expected: FAIL.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Teach the HTML service about the attribute**
|
||||||
|
|
||||||
|
Treat `api` as a known attribute on any element, and route completion inside its quotes to `apiCallCompletions`. The value is a call expression, not text — it must not be spell-checked or reformatted as prose.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun test packages/language-server
|
||||||
|
bun run format
|
||||||
|
git add packages/language-server
|
||||||
|
git commit -m "feat(language-server): understand the api binding attribute"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Diagnostics for the removed constructs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/language-server/src/diagnostics` entry point (wherever `.wrn` diagnostics are produced)
|
||||||
|
- Test: `packages/language-server/test/removed-construct-diagnostics.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: an `ssr { api … }` or `client { api … }` block yields a diagnostic naming `apis { }`, positioned on the block keyword.
|
||||||
|
|
||||||
|
The compiler already rejects these. The editor should say so while typing, and say what to do instead.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/language-server/test/removed-construct-diagnostics.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { diagnoseWrn } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("an ssr data block is flagged and names the replacement", () => {
|
||||||
|
const diagnostics = diagnoseWrn(`page P {
|
||||||
|
ssr { api x GET /api/x { response { return data } } }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(diagnostics.length).toBeGreaterThan(0);
|
||||||
|
expect(diagnostics[0]!.message).toContain("apis");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("client state is not flagged", () => {
|
||||||
|
const diagnostics = diagnoseWrn(`page P {
|
||||||
|
client state { count = 0 }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use whichever diagnostic entry point the language server exports; keep the assertions identical.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server/test/removed-construct-diagnostics.test.ts`
|
||||||
|
Expected: FAIL — either no diagnostic, or one that does not name `apis`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Surface the parse error as a diagnostic**
|
||||||
|
|
||||||
|
The parser already throws a message naming `apis { }` for these blocks. Ensure that message reaches the diagnostic with a position on the offending keyword rather than at offset zero.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun test packages/language-server
|
||||||
|
bun run format
|
||||||
|
git add packages/language-server
|
||||||
|
git commit -m "feat(language-server): flag the removed data blocks"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Answer the inline-diagnostics question, then the full gate
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: whatever the observation in Step 2 shows is needed, or none
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: Tasks 1-5.
|
||||||
|
|
||||||
|
The `apis` plan generates type assertions that make `tsc` fail when a block declares a field its endpoint rejects. **Whether that failure appears inside the `.wrn` file has never been confirmed** — it was inferred from reading source. This task settles it by looking.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rebuild the bundles**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Observe, and write down what you see**
|
||||||
|
|
||||||
|
In `examples/basic-app`, add a field to an `apis { }` entry that its endpoint does not accept, and run `bun run --cwd examples/basic-app wrnexus generate types`. Open the page in VS Code and record exactly where the error appears: on the block, only in `app/types/wrnexus.generated.api-checks.ts`, or nowhere.
|
||||||
|
|
||||||
|
Write the answer into the task report. **Do not skip this step and reason about it instead** — that is what left the question open the first time.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Act on what you observed**
|
||||||
|
|
||||||
|
If the error already surfaces usefully on the block, document it and stop.
|
||||||
|
|
||||||
|
If it appears only in the generated file, map the diagnostic back: the generator knows which page and block produced each assertion, so record that mapping when emitting and use it to relocate the diagnostic.
|
||||||
|
|
||||||
|
If that mapping proves larger than this task can hold, **stop and report it as follow-up work** rather than half-building it. Say so plainly in the report.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Remove the temporary field**
|
||||||
|
|
||||||
|
Revert the deliberate error and confirm `bun run typecheck` passes with zero net diff in `examples/basic-app`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Full gate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
bun test
|
||||||
|
bun run typecheck
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
bun run --cwd editors/vscode test
|
||||||
|
bun run check:production
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: exit 0 throughout, including `check:editor-compiler`, `check:editor-language-server`, and `check:editor-extension`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Manual pass, recorded**
|
||||||
|
|
||||||
|
Open the migrated `examples/basic-app` in VS Code and confirm: `apis { }` highlights, `api.` completes with the page's block names, hovering a name shows its method and path, and an `ssr { api … }` block is flagged. Record what you saw in the report — including anything that did not work.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat(editor): complete tooling support for the apis block"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for the executor
|
||||||
|
|
||||||
|
- **`client` is not one thing.** Removing every `client` rule from the grammar would break `client state`, `runtime = "client"`, and `client function`. Only the data-block patterns go.
|
||||||
|
- **The parser must tolerate half-typed input.** Completion fires while the document does not parse; a thrown error must become an empty completion list, not a failed request.
|
||||||
|
- **Step 2 of Task 6 is an observation, not a deduction.** Open the editor and look.
|
||||||
|
- **If a test would still pass with the code it guards deleted, it is not a test.**
|
||||||
@@ -0,0 +1,537 @@
|
|||||||
|
# Legacy and Config Cleanup Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Delete the compatibility-flag surface, the `"legacy"` function runtime, dead migrations, and deprecated APIs before the framework's first public release.
|
||||||
|
|
||||||
|
**Architecture:** Almost all of this is deletion. Seven config keys are never read by any code, so removing them changes nothing. The one behaviour-sensitive item is the `"legacy"` function runtime, which is mapped to `"shared"` — an equivalent substitution, because an unmarked function is already emitted into both bundles.
|
||||||
|
|
||||||
|
**Tech Stack:** Bun, TypeScript, `bun:test`.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-19-legacy-and-config-cleanup-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- This plan removes configuration. It adds none.
|
||||||
|
- The legacy `api` block forms (bare-body `with ($data)`, and `api` inside `ssr {}` / `client {}`) are **out of scope** — they are replaced by the next plan, not deleted here.
|
||||||
|
- A removed config key must be **rejected loudly**, not silently ignored. Someone with a stale config must be told, not left believing a flag still applies.
|
||||||
|
- `bun run format` before every commit; the repo gate is `bun run check:production`.
|
||||||
|
- The editor bundles embed the compiler — rebuild with `bun run --cwd editors/vscode build` after any `packages/syntax` or `packages/compiler` change, or `check:editor-compiler` fails on a stale bundle.
|
||||||
|
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files; escaping mangles them silently. Use file editing tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Replace the `"legacy"` function runtime with `"shared"`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/syntax/src/v060.ts` (the `FunctionRuntime` type; the default at ~line 209)
|
||||||
|
- Modify: `packages/compiler/src/client-codegen.ts` (membership tests at ~lines 173 and 340)
|
||||||
|
- Modify: `packages/compiler/src/server-codegen.ts` (the `["legacy", "server", "shared"]` list)
|
||||||
|
- Modify: `packages/compiler/src/codegen.ts` (`targetFunctions`, ~line 1310)
|
||||||
|
- Test: `packages/compiler/test/legacy-runtime-removal.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `FunctionRuntime` becomes `"client" | "server" | "shared"`. Later tasks and plans rely on `"legacy"` no longer existing.
|
||||||
|
|
||||||
|
**Why this is equivalent, not a behaviour change:** an unmarked `function foo()` currently parses as `"legacy"`, and both codegens include `"legacy"` in their membership tests — `["legacy", "client", "shared"]` for the browser and `["legacy", "server", "shared"]` for the server. So an unmarked function is already emitted into _both_ bundles, exactly like `shared`. `legacyDefaultRuntime` looks like it should modulate this but is never read.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/compiler/test/legacy-runtime-removal.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { parse } from "@wrnexus/syntax";
|
||||||
|
import { generateTargets } from "../src/targets.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Probe {
|
||||||
|
functions {
|
||||||
|
function unmarkedHelper() {
|
||||||
|
return "both";
|
||||||
|
}
|
||||||
|
|
||||||
|
client function clientOnly() {
|
||||||
|
return "browser";
|
||||||
|
}
|
||||||
|
|
||||||
|
server function serverOnly() {
|
||||||
|
return "server";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("an unmarked function is emitted into both the browser and server modules", () => {
|
||||||
|
// This is the property the "legacy" runtime provided. Removing the variant
|
||||||
|
// must not change it.
|
||||||
|
const targets = generateTargets(parse(SOURCE));
|
||||||
|
|
||||||
|
expect(targets.browser).toContain("unmarkedHelper");
|
||||||
|
expect(targets.server).toContain("unmarkedHelper");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("marked functions still go only where they belong", () => {
|
||||||
|
const targets = generateTargets(parse(SOURCE));
|
||||||
|
|
||||||
|
expect(targets.browser).toContain("clientOnly");
|
||||||
|
expect(targets.browser).not.toContain("serverOnly");
|
||||||
|
expect(targets.server).toContain("serverOnly");
|
||||||
|
expect(targets.server).not.toContain("clientOnly");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no emitted target mentions the removed legacy runtime", () => {
|
||||||
|
const targets = generateTargets(parse(SOURCE));
|
||||||
|
|
||||||
|
expect(targets.browser).not.toContain('"legacy"');
|
||||||
|
expect(targets.server).not.toContain('"legacy"');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test and record the baseline**
|
||||||
|
|
||||||
|
Run: `bun test packages/compiler/test/legacy-runtime-removal.test.ts`
|
||||||
|
Expected: the first two tests PASS (they describe current behaviour and must keep passing), the third may already pass. This test file is a **regression guard written before the change**, so a green run here is correct — record the output.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Remove the `"legacy"` variant from the type and parser**
|
||||||
|
|
||||||
|
In `packages/syntax/src/v060.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type FunctionRuntime = "client" | "server" | "shared";
|
||||||
|
```
|
||||||
|
|
||||||
|
And at the parse site (~line 209), change the default:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
let runtime: FunctionRuntime = "shared";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Drop `"legacy"` from every membership test**
|
||||||
|
|
||||||
|
In `packages/compiler/src/client-codegen.ts`, both occurrences:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
["client", "shared"].includes(fn.runtime),
|
||||||
|
```
|
||||||
|
|
||||||
|
In `packages/compiler/src/server-codegen.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const names = ast.runtimeFunctions
|
||||||
|
.filter((fn) => ["server", "shared"].includes(fn.runtime))
|
||||||
|
.map((fn) => fn.name);
|
||||||
|
```
|
||||||
|
|
||||||
|
In `packages/compiler/src/codegen.ts`, `targetFunctions`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const runtimes =
|
||||||
|
target === "browser" ? (["client", "shared"] as const) : (["server", "shared"] as const);
|
||||||
|
```
|
||||||
|
|
||||||
|
Search the repo for any remaining `"legacy"` in these packages and remove each — the string must not survive in `packages/syntax` or `packages/compiler`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/syntax packages/compiler`
|
||||||
|
Expected: PASS, including the three guards from Step 1. If the first two now fail, the substitution was not equivalent — stop and report rather than adjusting the test.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Rebuild the editor bundles and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
git add packages/syntax packages/compiler editors/vscode/src
|
||||||
|
git commit -m "refactor: replace the legacy function runtime with shared"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Delete the compatibility surface
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Delete: `packages/styles/src/compatibility.ts`
|
||||||
|
- Delete: `packages/cli/src/compatibility-command.ts`
|
||||||
|
- Delete: `packages/cli/test/compatibility.test.ts`
|
||||||
|
- Modify: `packages/styles/src/config.ts` (`FunctionsConfig` ~233, `CompatibilityConfig` ~242, `AppConfig extends CompatibilityPolicy` ~249, the `functions?:` and `compatibility?:` members, the `resolveCompatibility` validation ~619, and the `CompatibilityPolicy` import ~23)
|
||||||
|
- Modify: `packages/styles/src/index.ts` (the `./compatibility.ts` exports at ~lines 39-45)
|
||||||
|
- Modify: `packages/cli/src/index.ts` (dispatch at ~line 295, help text at ~line 75)
|
||||||
|
- Modify: `packages/cli/src/create.ts` (~lines 263, 278-283)
|
||||||
|
- Modify: `packages/cli/src/update.ts` (the config insertion string at ~line 389)
|
||||||
|
- Modify: `packages/styles/test/config.test.ts` (assertions on the removed keys)
|
||||||
|
- Modify: `examples/basic-app/wrnexus.config.ts`
|
||||||
|
- Test: `packages/styles/test/removed-config-keys.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing from Task 1.
|
||||||
|
- Produces: `AppConfig` no longer extends `CompatibilityPolicy` and has no `compatibility` or `functions` members. `@wrnexus/styles` no longer exports `resolveCompatibility`, `isCompatibilityDate`, `CURRENT_COMPATIBILITY_DATE`, `CURRENT_FRAMEWORK_BEHAVIOUR`, `CompatibilityPolicy`, or `CompatibilityReport`.
|
||||||
|
|
||||||
|
**These seven keys are never read.** `legacyEmit`, `legacyEventProps`, `legacyComponentDiscovery`, `stringLayouts`, and `legacyDefaultRuntime` appear only in the type declaration, `create.ts`, and `update.ts`. `compatibilityDate` and `frameworkBehaviour` feed only a printed report and one validation. Removing them changes no behaviour.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/styles/test/removed-config-keys.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { validateConfig } from "../src/config.ts";
|
||||||
|
|
||||||
|
// A stale config must fail loudly. Silently ignoring a removed key leaves
|
||||||
|
// someone believing a flag still applies.
|
||||||
|
const REMOVED = [
|
||||||
|
{ key: "compatibilityDate", config: { compatibilityDate: "2026-08-02" } },
|
||||||
|
{ key: "frameworkBehaviour", config: { frameworkBehaviour: 1 } },
|
||||||
|
{ key: "functions", config: { functions: { legacyDefaultRuntime: "current" } } },
|
||||||
|
{ key: "compatibility", config: { compatibility: { legacyEmit: false } } },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { key, config } of REMOVED) {
|
||||||
|
test(`a config still setting "${key}" is rejected with a message naming it`, () => {
|
||||||
|
const issues = validateConfig(config as never);
|
||||||
|
const match = issues.find((issue) => issue.path === key || issue.path.startsWith(`${key}.`));
|
||||||
|
|
||||||
|
expect(match).toBeDefined();
|
||||||
|
expect(match!.severity).toBe("error");
|
||||||
|
expect(match!.message.toLowerCase()).toContain("removed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a config without those keys is accepted", () => {
|
||||||
|
const issues = validateConfig({} as never);
|
||||||
|
|
||||||
|
expect(issues.filter((issue) => issue.severity === "error")).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If `validateConfig` is not the exported name in `packages/styles/src/config.ts`, use whichever function that module exports for validation and keep the assertions identical.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/styles/test/removed-config-keys.test.ts`
|
||||||
|
Expected: FAIL — the keys are currently accepted, so no issue is produced.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Delete the compatibility module and its command**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rm packages/styles/src/compatibility.ts packages/cli/src/compatibility-command.ts packages/cli/test/compatibility.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
In `packages/styles/src/index.ts`, remove the whole `./compatibility.ts` export block (both the value exports and the `export type` line).
|
||||||
|
|
||||||
|
In `packages/cli/src/index.ts`, remove the `case "compatibility":` dispatch and the `wrnexus compatibility …` line from the help text.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Remove the config members and add the rejections**
|
||||||
|
|
||||||
|
In `packages/styles/src/config.ts`: delete the `CompatibilityPolicy` import, the `FunctionsConfig` and `CompatibilityConfig` interfaces, the `functions?:` and `compatibility?:` members of `AppConfig`, `extends CompatibilityPolicy` on `AppConfig`, and the `resolveCompatibility` validation block.
|
||||||
|
|
||||||
|
Then add the rejections so a stale config fails loudly:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const REMOVED_CONFIG_KEYS = [
|
||||||
|
"compatibilityDate",
|
||||||
|
"frameworkBehaviour",
|
||||||
|
"functions",
|
||||||
|
"compatibility",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const key of REMOVED_CONFIG_KEYS) {
|
||||||
|
if ((config as Record<string, unknown>)[key] !== undefined) {
|
||||||
|
issues.push({
|
||||||
|
path: key,
|
||||||
|
severity: "error",
|
||||||
|
message: "was removed; delete it from the configuration",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Place this beside the other validation pushes, using whatever local variable that function accumulates issues in.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Stop scaffolding and inserting the keys**
|
||||||
|
|
||||||
|
In `packages/cli/src/create.ts`, delete the `compatibilityDate`, `frameworkBehaviour`, and `functions: { legacyDefaultRuntime: … }` lines from the generated config.
|
||||||
|
|
||||||
|
In `packages/cli/src/update.ts` (~line 389), remove `functions: { legacyDefaultRuntime: "current" },` and the whole `compatibility: { … },` fragment from the insertion string.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Trim the example app config**
|
||||||
|
|
||||||
|
In `examples/basic-app/wrnexus.config.ts`, delete `compatibilityDate`, `frameworkBehaviour`, `functions`, and `compatibility`.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Update the existing config tests**
|
||||||
|
|
||||||
|
`packages/styles/test/config.test.ts` asserts on the removed keys. Remove those assertions. Do not weaken any assertion that is still meaningful — if a test only existed to cover compatibility, delete the whole test.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/styles packages/cli`
|
||||||
|
Expected: PASS, including the new rejection tests.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add -A packages/styles packages/cli examples/basic-app
|
||||||
|
git commit -m "refactor: delete the compatibility config surface"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Drop migrations below 0.8.0
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/cli/src/update.ts` (all `Migration` entries with `version` below `"0.8.0"`)
|
||||||
|
- Test: `packages/cli/test/update-migration-floor.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing.
|
||||||
|
- Produces: the migration list starts at `0.8.0`.
|
||||||
|
|
||||||
|
`update.ts` holds 111 migrations reaching back to `0.2.8`. The framework is pre-public and the only projects run `0.8.x`, so everything below the floor is unreachable.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/update-migration-floor.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
test("no migration targets a version below 0.8.0", () => {
|
||||||
|
const source = readFileSync(join(import.meta.dir, "../src/update.ts"), "utf8");
|
||||||
|
const versions = [...source.matchAll(/version:\s*"([0-9.]+)"/g)].map((match) => match[1]!);
|
||||||
|
|
||||||
|
expect(versions.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const belowFloor = versions.filter((version) => {
|
||||||
|
const [major, minor] = version.split(".").map(Number);
|
||||||
|
return major! === 0 && minor! < 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(belowFloor).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/update-migration-floor.test.ts`
|
||||||
|
Expected: FAIL, listing the `0.2.x`–`0.7.x` versions.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Delete the migrations below the floor**
|
||||||
|
|
||||||
|
Remove every `Migration` object whose `version` is below `"0.8.0"`, along with any helper function that becomes unused as a result. Keep every `0.8.x` entry.
|
||||||
|
|
||||||
|
After deleting, search for now-unreferenced helpers in the file and remove them too — an unused private helper is dead code, and the linter will flag it.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS. Existing update tests that exercised removed migrations should be deleted with them; do not keep a test that asserts nothing.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify `update` still runs end to end**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd examples/basic-app wrnexus update --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: completes without error and reports no pending migrations for an app already at the current version. Paste the output into the commit body if it is short.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "chore: drop update migrations below 0.8.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Remove the deprecated compiler re-export shims
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/compiler/src/codegen.ts` (~line 20, the `./parser.ts` import)
|
||||||
|
- Modify: `packages/compiler/src/native-codegen.ts` (~line 1, the `./parser.ts` import)
|
||||||
|
- Delete: `packages/compiler/src/parser.ts`, `packages/compiler/src/tokenizer.ts`, `packages/compiler/src/types.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing.
|
||||||
|
- Produces: nothing new; imports move to `@wrnexus/syntax`.
|
||||||
|
|
||||||
|
**Order matters.** These three files are two-line re-exports marked deprecated, but `codegen.ts` and `native-codegen.ts` still import from them. Deleting the files first breaks the build.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Repoint the imports**
|
||||||
|
|
||||||
|
In `packages/compiler/src/codegen.ts`, change:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
|
||||||
|
```
|
||||||
|
|
||||||
|
to import the same names from `@wrnexus/syntax`. If the file already imports from `@wrnexus/syntax`, merge them into that one import rather than adding a second.
|
||||||
|
|
||||||
|
In `packages/compiler/src/native-codegen.ts`, change:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Attr, PageAst, ViewNode } from "./parser.ts";
|
||||||
|
```
|
||||||
|
|
||||||
|
the same way.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify nothing else imports the shims**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "from \"./parser.ts\"\|from \"./tokenizer.ts\"\|from \"./types.ts\"" packages/compiler/src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no output. If anything remains, repoint it before continuing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Delete the shims**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rm packages/compiler/src/parser.ts packages/compiler/src/tokenizer.ts packages/compiler/src/types.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/compiler && bun run typecheck`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rebuild the editor bundles and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
git add -A packages/compiler editors/vscode/src
|
||||||
|
git commit -m "refactor: drop the deprecated compiler re-export shims"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Remove the deprecated `@wrnexus/auth` options
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/auth/src/http/index.ts` (~lines 56-59)
|
||||||
|
- Modify: `packages/auth/src/plugin.ts` (~lines 55-58)
|
||||||
|
- Modify: `packages/auth/src/types.ts` (~line 423)
|
||||||
|
- Modify: `packages/auth/src/engine.ts` (~lines 163-165 and 179-181)
|
||||||
|
- Modify: `packages/auth/test/http.test.ts`, `packages/auth/test/plugin.test.ts`, `packages/auth/test/engine.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing.
|
||||||
|
- Produces: nothing new. Options are removed, not renamed.
|
||||||
|
|
||||||
|
**These are our own superseded options, not an out-of-date dependency.** The current form is already what `examples/auth-showcase/app/lib/auth.ts` uses — it passes `onSignedIn` / `onSignedOut` to `createAuthEngine`, which is correct and must not change. The deprecated members are the same names on _different_ option objects.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Confirm the blast radius before deleting**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "onSuccessfullSignUp" packages/ examples/ services/ | grep -v dist/
|
||||||
|
grep -rn "onSignedIn\|onSignedOut" packages/ examples/ --include=*.ts | grep -v "packages/auth/src" | grep -v dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `onSuccessfullSignUp` has zero references. The `onSignedIn` / `onSignedOut` hits are `packages/auth/test/http.test.ts`, `packages/auth/test/plugin.test.ts`, and `examples/auth-showcase/app/lib/auth.ts`. **The example is the correct `createAuthEngine` form and must be left alone.** Record what you found; if the results differ from this, stop and report before deleting anything.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove the option declarations**
|
||||||
|
|
||||||
|
Delete `onSignedIn` and `onSignedOut` (and their `@deprecated` comments) from the options interface in `packages/auth/src/http/index.ts` and from `packages/auth/src/plugin.ts`. Delete `onSuccessfullSignUp` from `packages/auth/src/types.ts`. Delete the `rpId` and `origin` members from both verification signatures in `packages/auth/src/engine.ts`.
|
||||||
|
|
||||||
|
Then remove the code that reads them. The `rpId` / `origin` values are already ignored — verification uses the values bound to the issued challenge — so removing them changes no behaviour.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update the tests that exercised the deprecated paths**
|
||||||
|
|
||||||
|
`packages/auth/test/http.test.ts` and `plugin.test.ts` pass the deprecated options. Rewrite each to use the `createAuthEngine` form where the test is still meaningful, and delete the test where its only purpose was to cover the deprecated alias.
|
||||||
|
|
||||||
|
`packages/auth/test/engine.test.ts` passes `rpId` / `origin` to verification. Remove those arguments; the assertions on the verification result should be unchanged, which is the evidence that the options were inert.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/auth && bun run typecheck`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Confirm no `@deprecated` markers remain in auth**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "@deprecated" packages/auth/src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no output.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add -A packages/auth
|
||||||
|
git commit -m "refactor: remove the deprecated auth options"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Full gate
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: whatever the gate reports as stale (generated types, public API baseline, editor bundles)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: Tasks 1-5.
|
||||||
|
- Produces: a green `check:production`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rebuild the editor bundles**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiler and language server are embedded there and both changed.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the full gate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
bun test
|
||||||
|
bun run typecheck
|
||||||
|
bun run check:production
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Regenerate anything the gate reports as stale**
|
||||||
|
|
||||||
|
`check:public-api` fails when exports change — and this plan removed several from `@wrnexus/styles`. Run `bun run generate:public-api`, then **read the diff and confirm it is removals only**. An unexpected addition means something was exported by accident.
|
||||||
|
|
||||||
|
`check:generated-types` may need `bun run --cwd examples/basic-app wrnexus generate types`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the gate until green**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run check:production
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: regenerate baselines after the legacy cleanup"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for the executor
|
||||||
|
|
||||||
|
- **The seven config keys are dead.** If you find code that actually reads one, stop and report — the spec's central claim would be wrong and the plan needs revisiting.
|
||||||
|
- **Task 1 is the only behaviour-sensitive change.** Its first two tests describe current behaviour and must pass both before and after. If they fail after, the substitution was not equivalent; report rather than editing the test.
|
||||||
|
- **The auth example is already correct.** `examples/auth-showcase` uses `createAuthEngine({ onSignedIn })`, which is the current API, not the deprecated one.
|
||||||
|
- **If a test would still pass with the code it guards deleted, it is not a test.** Delete the implementation, watch it fail, restore it.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,496 @@
|
|||||||
|
# `wrnexus update` Migration Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** One `wrnexus update` carries an existing project from today's syntax to the syntax left by the cleanup and `apis { }` plans — or refuses precisely, naming the file and the reason.
|
||||||
|
|
||||||
|
**Architecture:** These are new `Migration` entries in the existing `update.ts` framework, which already has dry-run support and a report that separates automatic changes from ones needing review. `.wrn` rewriting parses with `@wrnexus/syntax` and re-emits through `formatWrn`, both already imported there.
|
||||||
|
|
||||||
|
**Tech Stack:** Bun, TypeScript, `bun:test`, `@wrnexus/syntax`.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-19-update-migration-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- **A file is transformed correctly, or it is left untouched and reported.** There is no third outcome — never a partial rewrite.
|
||||||
|
- Every migration is **idempotent**: running it twice changes nothing the second time.
|
||||||
|
- **Dry-run reports exactly what a real run would change**, and writes nothing.
|
||||||
|
- A run with anything in `needsReview` or `parseFailures` **exits non-zero**, so a scripted upgrade cannot appear to succeed while leaving a project half-migrated.
|
||||||
|
- Migrations attach to the release that ships the breaking change, above the `0.8.0` floor.
|
||||||
|
- `bun run format` before every commit; the gate is `bun run check:production`.
|
||||||
|
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Remove the dead config keys
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/cli/src/update.ts` (add a `Migration`)
|
||||||
|
- Test: `packages/cli/test/migrate-config-keys.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: a migration with `id: "remove-dead-config-keys"`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/migrate-config-keys.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { afterEach, expect, test } from "bun:test";
|
||||||
|
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { runMigrations } from "../src/update.ts";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const CONFIG = `export default {
|
||||||
|
compatibilityDate: "2026-08-02",
|
||||||
|
frameworkBehaviour: 1,
|
||||||
|
functions: { legacyDefaultRuntime: "current" },
|
||||||
|
compatibility: { legacyEmit: false, stringLayouts: false },
|
||||||
|
observability: { sampleRate: 1 },
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
function project(): string {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-migrate-"));
|
||||||
|
roots.push(root);
|
||||||
|
mkdirSync(join(root, "app"), { recursive: true });
|
||||||
|
writeFileSync(join(root, "wrnexus.config.ts"), CONFIG);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("the removed keys are deleted and the rest is kept", async () => {
|
||||||
|
const root = project();
|
||||||
|
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
|
||||||
|
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
||||||
|
|
||||||
|
expect(config).not.toContain("compatibilityDate");
|
||||||
|
expect(config).not.toContain("frameworkBehaviour");
|
||||||
|
expect(config).not.toContain("legacyDefaultRuntime");
|
||||||
|
expect(config).not.toContain("legacyEmit");
|
||||||
|
expect(config).toContain("observability");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("running it twice changes nothing the second time", async () => {
|
||||||
|
const root = project();
|
||||||
|
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
|
||||||
|
const once = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
||||||
|
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
|
||||||
|
|
||||||
|
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toBe(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a dry run writes nothing", async () => {
|
||||||
|
const root = project();
|
||||||
|
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: true });
|
||||||
|
|
||||||
|
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toBe(CONFIG);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use whatever entry point `update.ts` exports for running migrations; if the name differs from `runMigrations`, adapt the calls and keep the assertions identical.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/migrate-config-keys.test.ts`
|
||||||
|
Expected: FAIL — the keys survive.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the migration**
|
||||||
|
|
||||||
|
Append to the migration list in `packages/cli/src/update.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
version: "0.9.0",
|
||||||
|
id: "remove-dead-config-keys",
|
||||||
|
description: "Delete compatibilityDate, frameworkBehaviour, functions, and compatibility",
|
||||||
|
apply(ctx) {
|
||||||
|
const file = join(ctx.appRoot, "wrnexus.config.ts");
|
||||||
|
if (!existsSync(file)) return;
|
||||||
|
|
||||||
|
const before = readFileSync(file, "utf8");
|
||||||
|
// Each key is a whole property line or block; removing the line leaves
|
||||||
|
// valid TypeScript because these are always object members.
|
||||||
|
const after = before
|
||||||
|
.replace(/^\s*compatibilityDate:.*\n/m, "")
|
||||||
|
.replace(/^\s*frameworkBehaviour:.*\n/m, "")
|
||||||
|
.replace(/^\s*functions:\s*\{[^}]*\},?\s*\n/m, "")
|
||||||
|
.replace(/^\s*compatibility:\s*\{[^}]*\},?\s*\n/m, "");
|
||||||
|
|
||||||
|
if (after === before) return;
|
||||||
|
|
||||||
|
ctx.report.changedAutomatically.push(`${file}: removed dead compatibility keys`);
|
||||||
|
if (!ctx.dryRun) writeFileSync(file, after, "utf8");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "feat(cli): migrate away the dead config keys"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Move `ssr { api … }` / `client { api … }` into `apis { }`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/cli/src/migrations/apis-block.ts`
|
||||||
|
- Modify: `packages/cli/src/update.ts` (register the migration)
|
||||||
|
- Test: `packages/cli/test/migrate-apis-block.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `migrateApisBlock(source: string): { source: string; changed: boolean } | { skip: string }` — a pure function over `.wrn` text, so it is testable without a filesystem. `skip` carries the human-readable reason.
|
||||||
|
|
||||||
|
Sectioned bodies carry across unchanged, because the payload is already bound to `data`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/migrate-apis-block.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { migrateApisBlock } from "../src/migrations/apis-block.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Search {
|
||||||
|
client {
|
||||||
|
api searchUsers POST /api/users {
|
||||||
|
request { body { name?: string } }
|
||||||
|
response { return data.users }
|
||||||
|
error { return [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("a client api entry moves into an apis block", () => {
|
||||||
|
const result = migrateApisBlock(SOURCE) as { source: string; changed: boolean };
|
||||||
|
|
||||||
|
expect(result.changed).toBe(true);
|
||||||
|
expect(result.source).toContain("apis {");
|
||||||
|
expect(result.source).toContain("searchUsers POST /api/users");
|
||||||
|
expect(result.source).not.toContain("client {\n api");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the sections survive unchanged", () => {
|
||||||
|
const result = migrateApisBlock(SOURCE) as { source: string };
|
||||||
|
|
||||||
|
expect(result.source).toContain("return data.users");
|
||||||
|
expect(result.source).toContain("return []");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("running it on migrated source changes nothing", () => {
|
||||||
|
const once = (migrateApisBlock(SOURCE) as { source: string }).source;
|
||||||
|
const twice = migrateApisBlock(once) as { source: string; changed: boolean };
|
||||||
|
|
||||||
|
expect(twice.changed).toBe(false);
|
||||||
|
expect(twice.source).toBe(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a name declared in both modes is skipped with a reason", () => {
|
||||||
|
const clash = `page P {
|
||||||
|
ssr { api dup GET /api/a { response { return data } } }
|
||||||
|
client { api dup GET /api/a { response { return data } } }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = migrateApisBlock(clash) as { skip: string };
|
||||||
|
|
||||||
|
expect(result.skip).toContain("dup");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/migrate-apis-block.test.ts`
|
||||||
|
Expected: FAIL — the module does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the transform**
|
||||||
|
|
||||||
|
Create `packages/cli/src/migrations/apis-block.ts`. Parse with `parse` from `@wrnexus/syntax` to find the entries and validate the file, collect every `api` entry from `ssr` / `client` blocks, detect duplicate names across modes and return `{ skip }` when found, then emit one `apis { }` block and delete the now-empty mode blocks. Re-emit through `formatWrn`.
|
||||||
|
|
||||||
|
Detect already-migrated input by checking whether the source has an `apis` block and no mode data blocks; return `{ source, changed: false }`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Register it**
|
||||||
|
|
||||||
|
Add a `Migration` with `id: "move-api-blocks"` that walks `app/**/*.wrn`, calls `migrateApisBlock`, and routes the outcome: a change goes to `changedAutomatically`, a `skip` goes to `needsReview` with the file and reason, and a `parse` failure goes to `parseFailures` with the file left untouched.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "feat(cli): migrate api entries into the apis block"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Move mode-scoped helpers into `functions { shared … }`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/cli/src/migrations/mode-functions.ts`
|
||||||
|
- Modify: `packages/cli/src/update.ts`
|
||||||
|
- Test: `packages/cli/test/migrate-mode-functions.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `migrateModeFunctions(source: string): { source: string; changed: boolean } | { skip: string }`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/migrate-mode-functions.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { migrateModeFunctions } from "../src/migrations/mode-functions.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Hello {
|
||||||
|
ssr {
|
||||||
|
functions {
|
||||||
|
function userNames(users) {
|
||||||
|
return users.map((user) => user.name).join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("a mode helper becomes a shared function", () => {
|
||||||
|
const result = migrateModeFunctions(SOURCE) as { source: string; changed: boolean };
|
||||||
|
|
||||||
|
expect(result.changed).toBe(true);
|
||||||
|
expect(result.source).toContain("shared function userNames");
|
||||||
|
expect(result.source).not.toContain("ssr {");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("running it again changes nothing", () => {
|
||||||
|
const once = (migrateModeFunctions(SOURCE) as { source: string }).source;
|
||||||
|
const twice = migrateModeFunctions(once) as { changed: boolean; source: string };
|
||||||
|
|
||||||
|
expect(twice.changed).toBe(false);
|
||||||
|
expect(twice.source).toBe(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a name that already exists at page level is skipped with a reason", () => {
|
||||||
|
const clash = `page P {
|
||||||
|
functions { shared function userNames() { return "" } }
|
||||||
|
ssr { functions { function userNames(users) { return "" } } }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = migrateModeFunctions(clash) as { skip: string };
|
||||||
|
|
||||||
|
expect(result.skip).toContain("userNames");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/migrate-mode-functions.test.ts`
|
||||||
|
Expected: FAIL — the module does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement and register**
|
||||||
|
|
||||||
|
Create the module following Task 2's shape: relocate each mode-scoped function into the page-level `functions { }` with the `shared` modifier, skipping the file with a reason when a name already exists there. Register a `Migration` with `id: "move-mode-functions"` that routes outcomes to the same three report buckets.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "feat(cli): migrate mode-scoped helpers to shared functions"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Detect legacy bare-body blocks and report them — do not rewrite
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/cli/src/migrations/legacy-api-body.ts`
|
||||||
|
- Modify: `packages/cli/src/update.ts`
|
||||||
|
- Test: `packages/cli/test/migrate-legacy-api-body.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `detectLegacyApiBodies(source: string): { name: string; freeIdentifiers: string[] }[]`.
|
||||||
|
|
||||||
|
**This transform is deliberately manual, and the test pins that.** A legacy bare body is evaluated inside `with ($data ?? {})`, so it references payload fields as bare identifiers. Converting `return userNames(users)` needs `data.users` — but **nothing in the source distinguishes `users` (payload) from `userNames` (page helper)**. The response shape belongs to the route, which may not be typed. A migration that guessed would emit code that compiles and is wrong.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/migrate-legacy-api-body.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { detectLegacyApiBodies } from "../src/migrations/legacy-api-body.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Hello {
|
||||||
|
ssr {
|
||||||
|
api ssrUsers GET /api/users/ssr {
|
||||||
|
return userNames(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("a legacy bare body is detected with its free identifiers", () => {
|
||||||
|
const found = detectLegacyApiBodies(SOURCE);
|
||||||
|
|
||||||
|
expect(found).toHaveLength(1);
|
||||||
|
expect(found[0]!.name).toBe("ssrUsers");
|
||||||
|
expect(found[0]!.freeIdentifiers).toContain("users");
|
||||||
|
expect(found[0]!.freeIdentifiers).toContain("userNames");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a sectioned block is not reported", () => {
|
||||||
|
const sectioned = `page P {
|
||||||
|
apis { x GET /api/x { response { return data.users } } }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(detectLegacyApiBodies(sectioned)).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/migrate-legacy-api-body.test.ts`
|
||||||
|
Expected: FAIL — the module does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement detection only**
|
||||||
|
|
||||||
|
Create the module. Find `api` entries whose `sections` is absent (the bare-body form), and collect the free identifiers in the body — identifiers that are not declared locally and are not JavaScript globals. Return them. **Write no transform.**
|
||||||
|
|
||||||
|
- [ ] **Step 4: Register a report-only migration**
|
||||||
|
|
||||||
|
Add a `Migration` with `id: "report-legacy-api-bodies"` that pushes one `needsReview` entry per block, naming the file, the block, and the identifiers, and leaves the file byte-identical. Have `wrnexus update` print a short line explaining why this one is manual: the payload fields cannot be told apart from page helpers without knowing the route's response shape.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Write the byte-identical test**
|
||||||
|
|
||||||
|
Add a test that runs the full migration over a fixture project containing a legacy bare body and asserts the file's contents are unchanged afterwards, and that the report names the block. **This is the most important test in the plan** — it pins that the migration does not attempt the rewrite.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "feat(cli): report legacy api bodies for manual migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Exit code, output order, and the end-to-end run
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/cli/src/update.ts` (the command's output and exit code)
|
||||||
|
- Test: `packages/cli/test/update-exit-code.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: Tasks 1-4.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/update-exit-code.test.ts` asserting that a project with a legacy bare body produces a non-zero exit, and a fully-migratable project produces zero. Use the same temp-project pattern as Task 1.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/update-exit-code.test.ts`
|
||||||
|
Expected: FAIL — the command currently exits zero regardless.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the output and exit code**
|
||||||
|
|
||||||
|
Print, in order: what changed, what needs review and why, what failed to parse. Exit non-zero when `needsReview` or `parseFailures` is non-empty.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Migrate the example app with the command alone**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd examples/basic-app wrnexus update
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: the `.wrn` pages are migrated by the tool, not by hand. **If the framework's own example cannot be migrated by the tool, the tool is not finished** — report that rather than editing the example manually.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify the migrated example**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd examples/basic-app build
|
||||||
|
bun test
|
||||||
|
bun run typecheck
|
||||||
|
bun run check:production
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat(cli): fail the update when a project needs manual review"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for the executor
|
||||||
|
|
||||||
|
- **Never half-rewrite a file.** Parse first; on failure, record and move on. If any part of a file's transform cannot complete, skip the whole file and report it.
|
||||||
|
- **Idempotency is not optional.** Every transform detects already-migrated input.
|
||||||
|
- **Task 4 writes no transform.** If you find yourself building one, stop — the spec explains why a correct automatic answer does not exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-execution note (2026-08-20)
|
||||||
|
|
||||||
|
**Step 4 of Task 5 did not prove what it was written to prove.** By the time it ran,
|
||||||
|
`examples/basic-app` had already been moved to the current syntax by hand in commit
|
||||||
|
`890d6106`, so `wrnexus update` migrated a no-op target: 0 changed, 0 needing review,
|
||||||
|
0 parse failures. The framework's own example therefore does NOT demonstrate the tool
|
||||||
|
against real legacy syntax.
|
||||||
|
|
||||||
|
The guarantee instead rests on fixture-based tests that drive the real `updateApp` /
|
||||||
|
`runUpdate` entry points over projects containing genuine `ssr {}` / `client {}` source
|
||||||
|
-- including the mixed-content page that only converges because `move-mode-functions`
|
||||||
|
is registered before `move-api-blocks`. That is adequate coverage, but it is a weaker
|
||||||
|
kind of evidence than the plan intended, and it is recorded here rather than quietly
|
||||||
|
counted as a pass.
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# HTML editing support for `.wrn` files — Design
|
||||||
|
|
||||||
|
**Date:** 2026-08-18
|
||||||
|
**Status:** Approved for implementation
|
||||||
|
**Scope:** HTML autocomplete, tag closing, hover, Emmet, and folding inside `view { }` blocks.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Writing markup in a `.wrn` file should feel like writing HTML. Today it does not: there is
|
||||||
|
syntax highlighting but no tag completion, no attribute completion, no tag closing, and no
|
||||||
|
tag-level folding.
|
||||||
|
|
||||||
|
The grammar already declares `embeddedLanguages` (`meta.embedded.block.html` → `html`), which is
|
||||||
|
why markup _highlights_. That mapping only affects tokenization — VS Code's HTML language
|
||||||
|
service does not run on `.wrn` documents, so none of the editing behaviour follows from it.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- **HTML formatting.** See "Formatting is deliberately excluded" below.
|
||||||
|
- Editor support outside VS Code beyond what standard LSP gives for free.
|
||||||
|
- Changing `.wrn` syntax or the compiler.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Question | Decision |
|
||||||
|
| ------------------- | ---------------------------------------------------------------------------- |
|
||||||
|
| Features | Tag/attribute completion, auto-close and rename tags, hover + Emmet, folding |
|
||||||
|
| Placement | Shared language server; only auto-close-on-type is VS Code-specific |
|
||||||
|
| Completion strategy | One merged list, WRNexus entries ranked above HTML |
|
||||||
|
| Region detection | Tolerant scanner over a virtual document, not the AST |
|
||||||
|
| HTML knowledge | `vscode-html-languageservice` |
|
||||||
|
| Formatting | Excluded — `formatWrn` already owns markup formatting |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Virtual HTML document
|
||||||
|
|
||||||
|
New module: `packages/language-server/src/html-regions.ts`, exporting
|
||||||
|
`virtualHtmlDocument(document)`.
|
||||||
|
|
||||||
|
Everything outside a `view { }` block is replaced by whitespace of **identical length**, with
|
||||||
|
newlines preserved. The virtual document therefore has the same size and the same line/column
|
||||||
|
geometry as the source, so a position in the source _is_ the position in the virtual document.
|
||||||
|
No mapping table and no translation layer.
|
||||||
|
|
||||||
|
This is deliberately **not** the same shape as the existing `virtualTypeScriptDocument`, which
|
||||||
|
compacts code and carries line mappings back to source. Compaction is necessary there because
|
||||||
|
the output must be valid TypeScript. HTML has no such requirement, so the simpler
|
||||||
|
offset-preserving form applies, and the class of off-by-one bugs that mapping tables produce
|
||||||
|
does not arise.
|
||||||
|
|
||||||
|
**The load-bearing invariant:** `virtualHtmlDocument(doc).text.length === doc.text.length`, with
|
||||||
|
newlines at identical offsets. If this breaks, every feature reports positions off by some
|
||||||
|
amount rather than failing loudly.
|
||||||
|
|
||||||
|
### Region detection
|
||||||
|
|
||||||
|
Region detection is a tolerant scanner, **not** the `@wrnexus/syntax` parser. Completion fires
|
||||||
|
while the document is being typed, which is exactly when it does not parse. The scanner finds
|
||||||
|
`view` followed by `{` and tracks brace depth to the matching close.
|
||||||
|
|
||||||
|
Two hazards it must handle, both of which defeat a naive implementation:
|
||||||
|
|
||||||
|
- **Apostrophes in text content.** `<p>it's fine</p>` — a scanner treating `'` as a string
|
||||||
|
delimiter anywhere will consider the rest of the file one open string and lose every later
|
||||||
|
region. Quotes are tracked only inside attribute values, never in text nodes.
|
||||||
|
- **Nested braces from interpolation.** `class={cond ? "a" : "b"}` and `{{ a: 1 }}` nest, so
|
||||||
|
depth must be counted rather than scanning for the next `}`.
|
||||||
|
|
||||||
|
WRNexus-specific syntax (`@click`, `client:visible`, `{expr}`) is **not** blanked. The HTML
|
||||||
|
service tolerates unknown attributes, and blanking would cost region fidelity for no gain.
|
||||||
|
|
||||||
|
**Caching** is keyed on document URI and version, so a burst of requests from one keystroke
|
||||||
|
costs a single scan.
|
||||||
|
|
||||||
|
## Completion
|
||||||
|
|
||||||
|
### The server becomes the single authority inside view blocks
|
||||||
|
|
||||||
|
`textDocument/completion` gains a context check: a position is "in HTML" exactly when the
|
||||||
|
virtual document is non-blank there, which costs one character lookup.
|
||||||
|
|
||||||
|
**Inside a view block**, one list is assembled from two sources:
|
||||||
|
|
||||||
|
| Source | `sortText` prefix | Content |
|
||||||
|
| ------- | ----------------- | ------------------------------------------------------------------------ |
|
||||||
|
| WRNexus | `0` | Components, their props/outputs/slots, directives (`@click`, `client:*`) |
|
||||||
|
| HTML | `1` | Tags, attributes, attribute values |
|
||||||
|
|
||||||
|
`sortText` drives ordering independently of the label, so components rank above HTML tags
|
||||||
|
without filtering anything out. **Outside a view block**, behaviour is unchanged: WRN keywords
|
||||||
|
plus workspace items.
|
||||||
|
|
||||||
|
The server already indexes components, props, outputs, and slots
|
||||||
|
(`buildWorkspaceCompletionItems` in `packages/language-server/src/workspace.ts`), so both halves
|
||||||
|
of the merge are already available to it.
|
||||||
|
|
||||||
|
**Deduplication on exact label match, WRNexus wins.** A component named `Table` and the HTML
|
||||||
|
`table` differ in case and both survive; a component that genuinely shadows an HTML tag name
|
||||||
|
resolves to the component.
|
||||||
|
|
||||||
|
### Trigger characters
|
||||||
|
|
||||||
|
The server currently declares `["<", "@", ":", "."]`. Attributes and values additionally need
|
||||||
|
`" "`, `"="`, `"\""`, and `"/"`.
|
||||||
|
|
||||||
|
### This fixes an existing bug
|
||||||
|
|
||||||
|
The extension's `completion.js` registers its own provider with `<` among its trigger
|
||||||
|
characters, and the language server answers `textDocument/completion` as well. VS Code
|
||||||
|
concatenates both today, producing duplicate entries and unpredictable ordering before HTML is
|
||||||
|
involved at all.
|
||||||
|
|
||||||
|
As part of this work the extension's provider returns nothing when the position is inside a view
|
||||||
|
block, and keeps its current behaviour elsewhere. One owner per context.
|
||||||
|
|
||||||
|
**Consequence to accept knowingly:** the server becomes authoritative for the richest completion
|
||||||
|
context, so future component-intelligence work belongs in the server rather than in
|
||||||
|
`completion.js`.
|
||||||
|
|
||||||
|
## Hover
|
||||||
|
|
||||||
|
`textDocument/hover` answers from the HTML service over the virtual document when the position
|
||||||
|
is inside a view region, giving MDN documentation for tags and attributes. Outside a view
|
||||||
|
region, existing hover behaviour is unchanged.
|
||||||
|
|
||||||
|
Where a position resolves to a WRNexus component or prop, the component's own detail wins over
|
||||||
|
any HTML entry of the same name, matching the completion precedence rule above.
|
||||||
|
|
||||||
|
## Tag handling
|
||||||
|
|
||||||
|
### Linked editing is standard LSP
|
||||||
|
|
||||||
|
Renaming `<div>` and having `</div>` follow is `textDocument/linkedEditingRange` (LSP 3.16), so
|
||||||
|
it lives in the shared server like everything else.
|
||||||
|
|
||||||
|
### Auto-close on type is the one client-side piece
|
||||||
|
|
||||||
|
LSP has no request for "close this tag as I type". VS Code's own HTML extension implements it
|
||||||
|
client-side, and this follows the same shape:
|
||||||
|
|
||||||
|
1. The extension subscribes to `onDidChangeTextDocument`, filtered to `wrn` documents.
|
||||||
|
2. When the typed character is `>` or `/`, it sends a custom request, `wrn/tagComplete`.
|
||||||
|
3. The server runs the HTML service's `doTagComplete` against the virtual document and returns a
|
||||||
|
snippet or `null`.
|
||||||
|
4. The client inserts it with `insertSnippet`, so the cursor lands between the tags.
|
||||||
|
|
||||||
|
The decision stays server-side because it needs parse knowledge: void elements (`<br>`, `<img>`,
|
||||||
|
`<input>`) must not be closed, and an already-closed tag must not be closed twice. Returning
|
||||||
|
`null` outside a view region is what stops it firing inside `functions { }` or `style { }`.
|
||||||
|
|
||||||
|
Component tags come along for free: `<Card>` closes to `</Card>` because the HTML service closes
|
||||||
|
unknown tags like any other, and `<Card /` completes to `<Card />` through the same `/` path.
|
||||||
|
|
||||||
|
**New setting:** `wrnexus.html.autoClosingTags`, default `true`, following the existing
|
||||||
|
`wrnexus.*` naming.
|
||||||
|
|
||||||
|
### Emmet
|
||||||
|
|
||||||
|
A manifest change: `emmet.includeLanguages: { "wrn": "html" }` in `contributes.configurationDefaults`.
|
||||||
|
|
||||||
|
**Known limitation:** `emmet.includeLanguages` is per-language, not per-region, so Emmet is also
|
||||||
|
live inside `functions { }` and `style { }` blocks. VS Code offers no way to scope it to a
|
||||||
|
region. Emmet only expands on Tab against an abbreviation pattern, so misfires are rare, but the
|
||||||
|
edge is real.
|
||||||
|
|
||||||
|
## Folding
|
||||||
|
|
||||||
|
`textDocument/foldingRange` in the server returns tag-level ranges from the HTML service over
|
||||||
|
the virtual document, filtered to view regions.
|
||||||
|
|
||||||
|
Today folding comes only from `language-configuration.json` markers, which work at block level
|
||||||
|
(`page`, `component`, `view`, braces). Markup does not fold, so a long `<table>` cannot be
|
||||||
|
collapsed. VS Code merges marker-based folding with provider ranges, so block folding continues
|
||||||
|
to work unchanged and tag folding appears inside markup.
|
||||||
|
|
||||||
|
**One rule:** return ranges only where the virtual document is non-blank. A range spanning
|
||||||
|
outside a view region would let a fold swallow a brace boundary.
|
||||||
|
|
||||||
|
## Formatting is deliberately excluded
|
||||||
|
|
||||||
|
`formatWrn` (`packages/syntax/src/formatter.ts`) is 927 lines, iterates to a fixed point with
|
||||||
|
cycle detection, and already handles tags, attribute wrapping, `multilineAttributes`, and
|
||||||
|
`printWidth`. It is a markup formatter that understands WRNexus syntax.
|
||||||
|
|
||||||
|
Adding HTML formatting would do two harmful things:
|
||||||
|
|
||||||
|
- **Two formatters would fight.** Output would depend on which ran last.
|
||||||
|
- **It would mangle syntax it does not model.** `@click={handler}` and `client:visible` are not
|
||||||
|
HTML attributes, and an HTML formatter is free to rewrite spacing inside them.
|
||||||
|
|
||||||
|
If markup formatting is unsatisfying, the fix is improving `formatWrn`. That is separate work.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
`vscode-html-languageservice` becomes a dependency of **both** `packages/language-server` and
|
||||||
|
`editors/vscode`.
|
||||||
|
|
||||||
|
The editor bundler (`scripts/build-editor-language-server.mjs`) bundles only workspace sources
|
||||||
|
and passes other `require`s through to Node, so the package must be resolvable at runtime from
|
||||||
|
the extension. `editors/vscode` currently ships exactly one runtime dependency
|
||||||
|
(`vscode-languageclient`); this adds the second.
|
||||||
|
|
||||||
|
`check:editor-language-server` already verifies the bundled `.cjs` starts under Node, so a
|
||||||
|
missing or unresolvable dependency fails the gate rather than shipping a broken VSIX.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Region scanner (`packages/language-server/test/`)
|
||||||
|
|
||||||
|
- **The invariant**, property-style across fixtures: virtual text length equals source length and
|
||||||
|
newlines sit at identical offsets.
|
||||||
|
- **Apostrophes in text**: `<p>it's fine</p>` followed by a second view block — both regions
|
||||||
|
found.
|
||||||
|
- **Nested interpolation**: `class={cond ? "a" : "b"}` and `{{ a: 1 }}` do not end the region.
|
||||||
|
- **Broken markup**: `<div class="` mid-typing still yields a region. This is the normal case for
|
||||||
|
completion, not an edge case.
|
||||||
|
- **Multiple view blocks**, and files with none.
|
||||||
|
|
||||||
|
### Completion
|
||||||
|
|
||||||
|
- Inside a view block: both sources present, WRNexus `sortText` ordering first.
|
||||||
|
- Outside a view block: response identical to current behaviour — the guard proving non-markup
|
||||||
|
contexts are undisturbed.
|
||||||
|
- Collision: a component named `Table` yields one entry, the component.
|
||||||
|
|
||||||
|
### Tag handling
|
||||||
|
|
||||||
|
- `<div>` → `</div>`; `<br>` → nothing; `<Card /` → `/>`; outside a view region → `null`.
|
||||||
|
- Linked editing returns ranges covering both the opening and closing tag names.
|
||||||
|
|
||||||
|
### Hover
|
||||||
|
|
||||||
|
- Inside a view region, a known tag returns HTML documentation.
|
||||||
|
- A component name returns the component detail, not an HTML entry of the same name.
|
||||||
|
|
||||||
|
### Folding
|
||||||
|
|
||||||
|
- Every returned range lies inside a view region.
|
||||||
|
- Block-level marker folding still works.
|
||||||
|
|
||||||
|
### Toolchain guards
|
||||||
|
|
||||||
|
- `check:editor-language-server` passes with the new dependency (bundle starts under Node).
|
||||||
|
- Manifest assertion that `emmet.includeLanguages` maps `wrn` → `html`, alongside the existing
|
||||||
|
marketplace checks in `editors/vscode/test`.
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
- HTML formatting — see above; improve `formatWrn` instead.
|
||||||
|
- Moving the remaining `completion.js` component intelligence into the server. This design only
|
||||||
|
requires it to stand down inside view blocks; relocating the rest is follow-up work.
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
# The `apis { }` block and location-transparent dispatch — Design
|
||||||
|
|
||||||
|
**Date:** 2026-08-19
|
||||||
|
**Status:** Approved for implementation
|
||||||
|
**Scope:** One container block for API declarations, callable from anywhere as `api.<name>(input)`,
|
||||||
|
dispatched in-process on the server and over `fetch` in the browser.
|
||||||
|
|
||||||
|
**Depends on:** `2026-08-19-legacy-and-config-cleanup-design.md`. That spec removes the
|
||||||
|
compatibility surface and the `"legacy"` function runtime; this one removes the legacy `api` forms
|
||||||
|
by replacing them.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Today a `.wrn` page has several unrelated ways to reach data: an `api` block inside `ssr {}`, an
|
||||||
|
`api` block inside `client {}`, a `server function` over RPC, a `load server` block, and hand-written
|
||||||
|
`fetch`. Each has different placement, different capabilities, and a different call shape. The
|
||||||
|
result is that "how do I fetch this?" has no single answer, and the answer that is right depends on
|
||||||
|
where the code happens to sit.
|
||||||
|
|
||||||
|
This collapses the API half of that into one declaration and one call, and draws a line a developer
|
||||||
|
can hold in their head:
|
||||||
|
|
||||||
|
- **`api.<name>()`** — call an API route that exists as a real HTTP endpoint.
|
||||||
|
- **`server.<name>()`** — call server-side logic that has no public surface.
|
||||||
|
|
||||||
|
The question becomes "is there a route?", not "where am I running?". `server.<name>()` is unchanged
|
||||||
|
by this spec.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Changing `server function`, actions, or `load` blocks. They keep working exactly as they do.
|
||||||
|
- External or third-party API targets. Still this app's `/api/*` routes only, preserving
|
||||||
|
`isSafeApiPath`.
|
||||||
|
- Any new configuration key. This spec adds none.
|
||||||
|
- Author-settable request headers, still excluded.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Question | Decision |
|
||||||
|
| ---------------------- | ---------------------------------------------------------------------- |
|
||||||
|
| Container | Page-level `apis { }`, matching `functions { }` |
|
||||||
|
| Declaration | Mode-less — no `ssr` / `client` prefix |
|
||||||
|
| Call | `api.<name>(input)` from any context |
|
||||||
|
| Dispatch | Chosen at build time: in-process on the server, `fetch` in the browser |
|
||||||
|
| Server request context | `AsyncLocalStorage` |
|
||||||
|
| Browser emission | Only blocks the client actually calls |
|
||||||
|
| Render binding | `api="name"`, `api="name()"`, `api="name({ … })"` |
|
||||||
|
| Old forms | Removed and replaced |
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
apis {
|
||||||
|
searchUsers POST /api/users {
|
||||||
|
request {
|
||||||
|
body {
|
||||||
|
name?: string
|
||||||
|
age?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response { return data.data.users }
|
||||||
|
error { return [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
listTeams GET /api/teams {
|
||||||
|
response { return data.data.teams }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET` and `HEAD` declare `parameters`, which become a query string; other methods declare `body`,
|
||||||
|
sent as JSON. The path stays a plain literal so `isSafeApiPath` is satisfied without relaxing it.
|
||||||
|
|
||||||
|
Names are unique per page — `codegen.ts` already rejects duplicates, and that stays.
|
||||||
|
|
||||||
|
### Why the container
|
||||||
|
|
||||||
|
`functions { }` puts the modifier first inside a container named for the concept:
|
||||||
|
`functions { client function x() }`. Today's api form inverts that — `client { api x … }` — and is
|
||||||
|
the only construct in the language shaped that way, which is why APIs are hard to find in a page.
|
||||||
|
`apis` (plural) is the container; `api` remains the call namespace and the binding attribute.
|
||||||
|
|
||||||
|
## Calling
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
functions {
|
||||||
|
client async function search(): Promise<void> {
|
||||||
|
users = await api.searchUsers({ name: nameFilter })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load server directory {
|
||||||
|
return await api.searchUsers({ name: ctx.url.searchParams.get("name") ?? "" })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same call works in client functions, `load` blocks, actions, and server functions. Nothing at
|
||||||
|
the call site says where it runs.
|
||||||
|
|
||||||
|
### Dispatch
|
||||||
|
|
||||||
|
Dispatch is decided at build time, not by a runtime check. The compiler emits an `api` object into
|
||||||
|
each execution context, with the same member names and different transports behind them:
|
||||||
|
|
||||||
|
- **Browser** — the existing `callApi` transport: query string or JSON body, `credentials:
|
||||||
|
"same-origin"`, `x-csrf-token` on non-GET, the established failure contract.
|
||||||
|
- **Server** — `callApiFromContext`, which dispatches to the route in-process. No network hop, no
|
||||||
|
serialisation round trip, and the caller's cookies, session, and locals are already forwarded.
|
||||||
|
|
||||||
|
Two objects that never meet, so nothing ships to the browser that only the server uses, and the
|
||||||
|
runtime never branches on `typeof window` for something known at compile time.
|
||||||
|
|
||||||
|
### `callApiFromContext` must learn to carry input
|
||||||
|
|
||||||
|
It currently builds `new Request(apiUrl, { method, headers })` — no body, no query string. It has to
|
||||||
|
assemble the request the same way the browser transport does, from the same rules, or the two sides
|
||||||
|
will disagree about what an identical call sends. **The assembly rules must be shared, not
|
||||||
|
reimplemented**: a second copy will drift, and the drift will be silent because each side is tested
|
||||||
|
separately.
|
||||||
|
|
||||||
|
### The request context
|
||||||
|
|
||||||
|
A server-side call needs `ctx` to resolve the URL and forward cookies and session. `ctx` is not
|
||||||
|
uniformly available: `load` blocks have it, schema actions have it as a second parameter, plain
|
||||||
|
actions and server functions have neither.
|
||||||
|
|
||||||
|
The server stores the request context in an `AsyncLocalStorage` at request entry, and the server
|
||||||
|
`api` object reads it. This is new machinery — the framework uses none today — and it must be
|
||||||
|
established in both the dev server and the production server, or a call that works in development
|
||||||
|
fails in production.
|
||||||
|
|
||||||
|
When no context is present, the call throws with a message naming the block and explaining that an
|
||||||
|
API call needs a request context — never a silent `undefined`.
|
||||||
|
|
||||||
|
## Emission
|
||||||
|
|
||||||
|
A block's `response` and `error` bodies are page code. They ship to the browser **only when a
|
||||||
|
client-side call to that block exists**. The compiler already knows which `api.<name>()` calls
|
||||||
|
appear in client functions.
|
||||||
|
|
||||||
|
This keeps server-only transforms off the wire and the client bundle proportional to what it uses.
|
||||||
|
The consequence to know: adding the first client call to a block starts shipping that block's
|
||||||
|
bodies. Anything secret belongs in the endpoint, not in a `response` body.
|
||||||
|
|
||||||
|
## Render binding
|
||||||
|
|
||||||
|
A block can be bound into markup, which calls it during render and substitutes the result:
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
<p api="listTeams">loading…</p>
|
||||||
|
<ul api="searchUsers({ name: nameFilter })">
|
||||||
|
{#each searchUsers as person}<li>{person.name}</li>{/each}
|
||||||
|
</ul>
|
||||||
|
```
|
||||||
|
|
||||||
|
Three accepted forms: `api="name"`, `api="name()"` — equivalent — and `api="name({ … })"`, which
|
||||||
|
passes arguments. The argument expression is evaluated in the same scope as other view expressions
|
||||||
|
at render time. This mirrors `@click="search()"`, so it introduces no new escaping or naming rules.
|
||||||
|
|
||||||
|
### The edge this creates, stated plainly
|
||||||
|
|
||||||
|
A block that is both render-bound and called from code **runs twice** — once for the binding, once
|
||||||
|
for the call. They are two different lifecycles wearing one name, and no deduplication is attempted:
|
||||||
|
a render-time fetch and a user-triggered fetch are usually meant to be different requests, and
|
||||||
|
silently collapsing them would be worse than the duplication. Authors binding a block _and_ calling
|
||||||
|
it should expect two requests.
|
||||||
|
|
||||||
|
## Replacing the old forms
|
||||||
|
|
||||||
|
`ssr { … }` and `client { … }` data blocks are removed. Each could contain only two things, and both
|
||||||
|
have a home:
|
||||||
|
|
||||||
|
| Old | New |
|
||||||
|
| ------------------------------------------------ | ------------------------------------------------------ |
|
||||||
|
| `ssr { api x … }` / `client { api x … }` | `apis { x … }` |
|
||||||
|
| `ssr { functions { function helper() } }` | `functions { shared function helper() }` |
|
||||||
|
| legacy bare-body `api x GET /p { return users }` | `apis { x GET /p { response { return data.users } } }` |
|
||||||
|
|
||||||
|
The legacy bare body injected the payload with `with ($data ?? {})`, which is **untypeable** —
|
||||||
|
TypeScript cannot see through `with`, and that is the entire reason sectioned blocks bind a named
|
||||||
|
`data`. Removing the legacy form removes that fork: one payload binding, typed.
|
||||||
|
|
||||||
|
`client state { }` is a different construct that shares the keyword and is **not** affected.
|
||||||
|
|
||||||
|
`examples/basic-app/app/pages/hello.wrn` uses both old forms and is the migration's worked example.
|
||||||
|
|
||||||
|
## Type safety
|
||||||
|
|
||||||
|
Assertions are generated into `app/types/wrnexus.generated.api-checks.ts` and checked by the
|
||||||
|
project's own `tsc`, as established. Two changes follow from mode-less declarations:
|
||||||
|
|
||||||
|
- The current generator skips blocks whose `mode !== "client"`. That skip exists because an `ssr`
|
||||||
|
block could never declare a `request`. Mode-less blocks invalidate the reasoning, so **every block
|
||||||
|
with declared fields gets an assertion**.
|
||||||
|
- The zero-field skip stays: a block with no declared fields has nothing to check, and asserting
|
||||||
|
`Record<string, never>` against a contract fails spuriously.
|
||||||
|
|
||||||
|
## Known edges
|
||||||
|
|
||||||
|
- **`state api` stops working.** The name is currently excluded from destructuring only when a page
|
||||||
|
has client api blocks, so pages without them keep using it. Once `api` is universal that
|
||||||
|
protection goes, and a page with `state api` breaks. It is a build-time failure, not silent.
|
||||||
|
- **Every block is browser-reachable in principle.** The routes were already publicly reachable by
|
||||||
|
`fetch`, so this exposes no new surface — but a block is no longer implicitly server-only by
|
||||||
|
virtue of its placement.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
**Parser**
|
||||||
|
|
||||||
|
- `apis { }` parses multiple mode-less entries; duplicate names are rejected.
|
||||||
|
- `ssr { api … }` and `client { api … }` are rejected with a message naming the replacement.
|
||||||
|
- All three binding forms parse, including an argument expression containing a nested object.
|
||||||
|
|
||||||
|
**Dispatch**
|
||||||
|
|
||||||
|
- A client-side call issues one `fetch` with the expected URL, method, body, and CSRF header.
|
||||||
|
- A server-side call dispatches in-process and issues **no** network request — asserted by observing
|
||||||
|
that no fetch occurs, not merely that the result is right.
|
||||||
|
- A server-side call with no request context throws a message naming the block.
|
||||||
|
- The same declared input produces the same request on both sides — the shared-assembly guard.
|
||||||
|
|
||||||
|
**Emission**
|
||||||
|
|
||||||
|
- A block called only from server code does not appear in the browser module.
|
||||||
|
- A block called from a client function does.
|
||||||
|
|
||||||
|
**Render binding**
|
||||||
|
|
||||||
|
- Each of the three forms renders the resolved value.
|
||||||
|
- A bound block that is also called issues two requests, pinning the documented edge.
|
||||||
|
|
||||||
|
**Type safety**
|
||||||
|
|
||||||
|
- A block declaring a field its endpoint rejects fails `bun run typecheck`, asserted by running
|
||||||
|
`tsc` and reading its diagnostics — not by matching generated text.
|
||||||
|
|
||||||
|
**End to end**
|
||||||
|
|
||||||
|
- `examples/basic-app` migrated to `apis { }`, driven in a browser: a client call updates state, a
|
||||||
|
render-bound block appears in the served HTML, and a deliberately failing call takes the `error`
|
||||||
|
path.
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
- External API targets, and the allowlist and credential handling they need.
|
||||||
|
- Author-settable headers.
|
||||||
|
- Deduplicating a render-bound block against a code call.
|
||||||
|
- Response caching and request de-duplication.
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# Editor support for the new syntax — Design
|
||||||
|
|
||||||
|
**Date:** 2026-08-19
|
||||||
|
**Status:** Approved for implementation
|
||||||
|
**Scope:** Language server and VS Code extension updated for `apis { }`, the `api.<name>()` call, and
|
||||||
|
the removal of the mode data blocks.
|
||||||
|
|
||||||
|
**Depends on:** `2026-08-19-apis-block-design.md`. The syntax must exist before the editor can
|
||||||
|
describe it.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
A syntax change that the editor does not know about is worse than no change: valid code is
|
||||||
|
red-underlined, removed constructs still autocomplete, and the new block gets no highlighting. This
|
||||||
|
spec keeps the tooling level with the language.
|
||||||
|
|
||||||
|
It also settles a question left open by earlier work: whether type errors from the generated
|
||||||
|
assertions actually appear inside the `.wrn` file, or only in the generated file. That was never
|
||||||
|
verified — it was inferred from reading source — and inference is not good enough for the thing
|
||||||
|
developers rely on to tell them their code is wrong.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- New editor features unrelated to this syntax change.
|
||||||
|
- Editors other than VS Code beyond what standard LSP provides.
|
||||||
|
- HTML formatting — `formatWrn` still owns markup.
|
||||||
|
|
||||||
|
## What exists
|
||||||
|
|
||||||
|
- **Grammar:** `editors/vscode/syntaxes/wrn.tmLanguage.json` names block keywords directly —
|
||||||
|
`api` appears 4 times, `client` 6, `server` 5, `ssr` once.
|
||||||
|
- **Keyword list:** `WRN_KEYWORDS` in `packages/language-server/src/index.ts` drives completion and
|
||||||
|
includes `api` but not `apis`.
|
||||||
|
- **Extension completions:** `editors/vscode/src/completion.js` carries block snippets.
|
||||||
|
- **Server features:** completion, hover, folding, linked editing, tag completion, and TypeScript
|
||||||
|
diagnostics over a virtual document.
|
||||||
|
|
||||||
|
## Changes by surface
|
||||||
|
|
||||||
|
### Grammar
|
||||||
|
|
||||||
|
Add `apis` as a block keyword. Remove the `ssr` / `client` **data block** patterns, keeping `client`
|
||||||
|
and `server` where they mean other things — `client state { }`, the `runtime` values, and the
|
||||||
|
function modifiers in `functions { }`. This is the change most likely to over-reach: `client` is one
|
||||||
|
word with several jobs in this language, and blanket removal would un-highlight constructs that
|
||||||
|
still exist.
|
||||||
|
|
||||||
|
Highlight an `apis` entry's shape — name, method, path — so a declaration reads as a declaration
|
||||||
|
rather than as loose identifiers.
|
||||||
|
|
||||||
|
### Keyword and block completion
|
||||||
|
|
||||||
|
- `apis` joins `WRN_KEYWORDS`.
|
||||||
|
- A snippet for the container and a snippet for an entry, including the `request` / `response` /
|
||||||
|
`error` sections, so the shape is discoverable without the docs.
|
||||||
|
- `ssr` and `client` data-block snippets are removed from `completion.js`. Offering a construct the
|
||||||
|
compiler rejects is worse than offering nothing.
|
||||||
|
|
||||||
|
### Call completion — the feature worth building
|
||||||
|
|
||||||
|
Inside a function body, `api.` should complete to the names declared in that page's `apis { }`
|
||||||
|
block, with the method and path as detail. The server already indexes the document to build
|
||||||
|
completions, and the block names are in the AST.
|
||||||
|
|
||||||
|
This is where the syntax pays off in the editor: the set of legal calls is knowable, so the editor
|
||||||
|
should know it. Without it, `api.` is an empty namespace and every call is typed from memory.
|
||||||
|
|
||||||
|
Hovering a name inside `api.<name>()` shows its method, path, and declared request fields.
|
||||||
|
|
||||||
|
### The `api=` attribute in markup
|
||||||
|
|
||||||
|
`api="searchUsers({ name: nameFilter })"` is an attribute whose value is a call expression. The HTML
|
||||||
|
service must not flag it as an unknown attribute, and the expression must not be treated as plain
|
||||||
|
text. Completion inside the quotes offers the page's block names, matching the `api.` behaviour.
|
||||||
|
|
||||||
|
### Diagnostics for removed constructs
|
||||||
|
|
||||||
|
An `ssr { api … }` or `client { api … }` block gets a diagnostic naming `apis { }` as the
|
||||||
|
replacement, positioned on the block keyword. The compiler already rejects these; the editor should
|
||||||
|
say so while typing rather than at build time, and it should say what to do instead.
|
||||||
|
|
||||||
|
## Inline type errors — verifying, not assuming
|
||||||
|
|
||||||
|
The `apis` spec generates assertions into `app/types/wrnexus.generated.api-checks.ts`, and `tsc`
|
||||||
|
fails when a block declares a field its endpoint rejects. Whether that failure surfaces **inside the
|
||||||
|
`.wrn` file** has never been confirmed.
|
||||||
|
|
||||||
|
This spec resolves it in two steps, in order:
|
||||||
|
|
||||||
|
1. **Observe the current behaviour.** With a deliberately wrong field in place, open the page in VS
|
||||||
|
Code and record where the error appears: on the block, only in the generated file, or nowhere.
|
||||||
|
2. **Act on what is observed.** If the error already surfaces usefully, document it and stop. If it
|
||||||
|
appears only in the generated file, map the diagnostic back to the block that produced it — the
|
||||||
|
generator knows which page and block each assertion came from, so the mapping is available if it
|
||||||
|
is recorded rather than discarded.
|
||||||
|
|
||||||
|
If step 2 proves larger than this spec can hold, it becomes its own work, and the spec says so
|
||||||
|
plainly rather than leaving an unfinished feature implied. **Nothing here should claim inline
|
||||||
|
diagnostics work until someone has seen them work.**
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
**Grammar** — a fixture page using `apis { }`, `client state { }`, `functions { shared function }`,
|
||||||
|
and `runtime = "client"` tokenizes correctly; `client` keeps its highlighting everywhere it is still
|
||||||
|
valid. This is the guard against over-reaching removal.
|
||||||
|
|
||||||
|
**Completion**
|
||||||
|
|
||||||
|
- `apis` is offered at page level; `ssr` / `client` data blocks are not.
|
||||||
|
- `api.` inside a function body offers the page's declared names with method and path.
|
||||||
|
- Inside `api="…"` in markup, the same names are offered.
|
||||||
|
- Outside those contexts, completion is unchanged — the guard that non-API editing is undisturbed.
|
||||||
|
|
||||||
|
**Hover** — a name inside `api.<name>()` reports its method, path, and request fields.
|
||||||
|
|
||||||
|
**Diagnostics** — an `ssr { api … }` block produces a diagnostic naming `apis { }`, positioned on
|
||||||
|
the block keyword.
|
||||||
|
|
||||||
|
**Bundles** — `check:editor-compiler`, `check:editor-language-server`, and
|
||||||
|
`check:editor-extension` pass. These embed the compiler and language server, so they must be rebuilt
|
||||||
|
after the syntax change; a stale bundle fails the gate.
|
||||||
|
|
||||||
|
**Manual, and recorded in the implementation notes** — open the migrated `examples/basic-app` in VS
|
||||||
|
Code: `apis { }` highlights, `api.` completes, a removed construct is flagged, and the inline
|
||||||
|
type-error question above is answered by observation.
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
- Mapping generated assertion diagnostics back into `.wrn`, if step 2 above proves too large.
|
||||||
|
- Moving the remaining component intelligence out of `completion.js` and into the server.
|
||||||
|
- Editors other than VS Code.
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Legacy, deprecated, and unused-config cleanup — Design
|
||||||
|
|
||||||
|
**Date:** 2026-08-19
|
||||||
|
**Status:** Approved for implementation
|
||||||
|
**Scope:** Remove the compatibility-flag surface, the `"legacy"` function runtime, dead migrations,
|
||||||
|
and deprecated APIs — before the framework's first public release.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
WRNexus carries compatibility machinery for a public it does not yet have. Every branch of it is
|
||||||
|
either switched off in the only apps that exist, or wired to nothing at all. Removing it now costs
|
||||||
|
almost nothing; removing it after release costs a major version and other people's time.
|
||||||
|
|
||||||
|
## Why this is safe now
|
||||||
|
|
||||||
|
Two pieces of evidence, both verified rather than assumed:
|
||||||
|
|
||||||
|
**The only consumers already run without it**, which matters for the config files themselves even
|
||||||
|
though the keys are inert. `D:\Company\wrnexus\apps\admin` and
|
||||||
|
`D:\Company\wrnexus\apps\web` — both test projects, neither deployed — set every compatibility flag
|
||||||
|
to `false` and `legacyDefaultRuntime: "current"`. They are already on the modern path; removing the
|
||||||
|
flags means deleting the lines that say "off".
|
||||||
|
|
||||||
|
**None of the seven keys change any behaviour.** Verified by tracing every reference, not by
|
||||||
|
reading the types. `legacyEmit`, `legacyEventProps`, `legacyComponentDiscovery`, `stringLayouts`,
|
||||||
|
and `legacyDefaultRuntime` appear in exactly three places each: the type declaration in
|
||||||
|
`config.ts`, the scaffolder in `create.ts`, and the insertion in `update.ts`. **No compiler,
|
||||||
|
codegen, or runtime code reads any of them.** They are written into every generated config and then
|
||||||
|
ignored.
|
||||||
|
|
||||||
|
The remaining two are the same story with more machinery. `resolveCompatibility` (`packages/styles/src/compatibility.ts`)
|
||||||
|
produces a report — an effective date, a behaviour number, and advisory strings. Tracing every
|
||||||
|
consumer: the `wrnexus compatibility` command prints it, and `config.ts` raises one validation error
|
||||||
|
when the configured date is _newer_ than the CLI supports. **No compiler branch and no runtime
|
||||||
|
behaviour reads `effectiveDate` or `effectiveBehaviour`.** The mechanism is scaffolding that was
|
||||||
|
never connected.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- **The legacy `api` block forms.** Bare-body blocks using `with ($data)`, and `api` entries inside
|
||||||
|
`ssr {}` / `client {}`, are _replaced_ rather than deleted — that is the next spec's job. Removing
|
||||||
|
them here would leave a gap with no working mechanism.
|
||||||
|
- Adding any configuration key. This spec only removes them.
|
||||||
|
- Changing any behaviour that is currently switched on.
|
||||||
|
|
||||||
|
## What gets removed
|
||||||
|
|
||||||
|
| Item | Where | Why it goes |
|
||||||
|
| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
|
||||||
|
| `compatibility: { legacyEmit, legacyEventProps, legacyComponentDiscovery, stringLayouts }` | `packages/styles/src/config.ts` (`CompatibilityConfig`) | Never read by any code |
|
||||||
|
| `functions: { legacyDefaultRuntime }` | `packages/styles/src/config.ts` (`FunctionsConfig`) | Never read by any code |
|
||||||
|
| `compatibilityDate` | `packages/styles/src/compatibility.ts` (`CompatibilityPolicy`) | Gates nothing |
|
||||||
|
| `frameworkBehaviour` | same | Gates nothing |
|
||||||
|
| `resolveCompatibility`, `CompatibilityReport`, `isCompatibilityDate`, `CURRENT_COMPATIBILITY_DATE`, `CURRENT_FRAMEWORK_BEHAVIOUR` | `packages/styles/src/compatibility.ts` | Whole module serves only the two dead keys |
|
||||||
|
| `wrnexus compatibility <check\|explain\|upgrade>` | `packages/cli/src/compatibility-command.ts`, dispatch at `packages/cli/src/index.ts:295`, help text at `:75` | Reports on removed keys |
|
||||||
|
| `"legacy"` variant of `FunctionRuntime` | `packages/syntax/src/v060.ts`, branches in `packages/compiler/src/client-codegen.ts` and `server-codegen.ts` | An unmarked function becomes `shared` (see below) |
|
||||||
|
| Migrations below `0.8.0` | `packages/cli/src/update.ts` | 111 migrations reach back to `0.2.8`; no project exists below `0.8.x` |
|
||||||
|
| Deprecated re-export shims | `packages/compiler/src/{parser,tokenizer,types}.ts` | Two lines each, re-exporting `@wrnexus/syntax` |
|
||||||
|
| Deprecated `@wrnexus/auth` options | `engine.ts` (4 sites), `http/index.ts` (2), `plugin.ts` (2), `types.ts` (1) | See "Deprecated auth options" |
|
||||||
|
|
||||||
|
## The `"legacy"` function runtime
|
||||||
|
|
||||||
|
`FunctionRuntime` is `"legacy" | "client" | "server" | "shared"`. `"legacy"` is what an _unmarked_
|
||||||
|
`function foo()` gets, and `legacyDefaultRuntime` decides how it behaves. Both codegens then test
|
||||||
|
membership: `["legacy", "client", "shared"]` for the browser and `["legacy", "server", "shared"]`
|
||||||
|
for the server — which is to say **an unmarked function is currently emitted into both bundles,
|
||||||
|
exactly like `shared`.**
|
||||||
|
|
||||||
|
`legacyDefaultRuntime` looks like it should modulate this, but it is never read (above), so the
|
||||||
|
mapping is unconditional: unmarked is always `"legacy"`, and `"legacy"` is always emitted to both
|
||||||
|
bundles.
|
||||||
|
|
||||||
|
So the removal is mechanical: delete the `"legacy"` variant, and parse an unmarked function as
|
||||||
|
`"shared"`. The emitted output for every existing unmarked function is unchanged, in every
|
||||||
|
configuration. `FunctionRuntime`
|
||||||
|
becomes `"client" | "server" | "shared"`, and the membership tests lose one element each.
|
||||||
|
|
||||||
|
This is the one item where behaviour could drift if done carelessly, so its test is explicit: an
|
||||||
|
unmarked function must still appear in both the browser and server modules.
|
||||||
|
|
||||||
|
## Deprecated re-export shims
|
||||||
|
|
||||||
|
`packages/compiler/src/parser.ts`, `tokenizer.ts`, and `types.ts` are two-line files re-exporting
|
||||||
|
`@wrnexus/syntax`. They are marked deprecated, but **`codegen.ts` and `native-codegen.ts` still
|
||||||
|
import from them**, so deleting the files is not enough — those imports must be repointed at
|
||||||
|
`@wrnexus/syntax` first. Removing the files without that step breaks the build.
|
||||||
|
|
||||||
|
## Deprecated auth options
|
||||||
|
|
||||||
|
`@wrnexus/auth` carries nine `@deprecated` markers. These are **our own superseded options**, not an
|
||||||
|
out-of-date dependency — there is no newer version to move to, only newer options we already added.
|
||||||
|
Removing them means deleting the old aliases and moving the few call sites that still use them.
|
||||||
|
|
||||||
|
| Group | Where | Replacement | Still used? |
|
||||||
|
| --------------------------------------------------------------------- | ---------------------------- | ------------------------------------------- | ----------------------------------------------------------- |
|
||||||
|
| `onSignedIn` / `onSignedOut` on HTTP route options and plugin options | `http/index.ts`, `plugin.ts` | the same names on `createAuthEngine({ … })` | only `packages/auth/test/http.test.ts` and `plugin.test.ts` |
|
||||||
|
| `onSuccessfullSignUp` (misspelled alias) | `types.ts` | `onSuccessfulSignUp` | nowhere — zero references |
|
||||||
|
| `rpId` / `origin` on passkey verification | `engine.ts` (4 sites) | values bound to the issued challenge | `packages/auth/test/engine.test.ts` |
|
||||||
|
|
||||||
|
Two things this table settles:
|
||||||
|
|
||||||
|
- **The recommended form is already in use.** `examples/auth-showcase/app/lib/auth.ts` passes
|
||||||
|
`onSignedIn` / `onSignedOut` to `createAuthEngine`, which is the _current_ API. The deprecated
|
||||||
|
members are the same names on different option objects, so the example needs no change.
|
||||||
|
- **Neither test app uses any of them.** Nothing in `D:\Company\wrnexuspps` references these
|
||||||
|
options.
|
||||||
|
|
||||||
|
So the blast radius is three test files inside `packages/auth`, which are exercising the deprecated
|
||||||
|
paths and are updated or removed alongside them. The `rpId` / `origin` options are already ignored at
|
||||||
|
runtime — verification uses the values bound to the issued challenge — so removing them changes no
|
||||||
|
behaviour, only the shape of the call.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
`examples/basic-app` and both test apps need one pass each:
|
||||||
|
|
||||||
|
1. Delete `compatibility`, `functions`, `compatibilityDate`, and `frameworkBehaviour` from
|
||||||
|
`wrnexus.config.ts` — seven lines per app.
|
||||||
|
2. `packages/cli/src/create.ts` stops scaffolding those keys, so new apps get a shorter config.
|
||||||
|
|
||||||
|
No `.wrn` source changes. Nothing in this spec alters page syntax.
|
||||||
|
|
||||||
|
The removed `0.2.x`–`0.7.x` migrations mean a project below `0.8.0` can no longer be upgraded by
|
||||||
|
`wrnexus update`. No such project exists, and rescuing one would be a manual job either way.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **The `"legacy"` runtime removal is behaviour-preserving**: an unmarked function still appears in
|
||||||
|
both the browser and server modules. This is the assertion most worth writing, because it is the
|
||||||
|
only removal that could silently change output.
|
||||||
|
- **Config rejects the removed keys** rather than ignoring them, so a stale config fails loudly with
|
||||||
|
a message naming the key. A silently-ignored key would leave someone believing a flag still
|
||||||
|
applies.
|
||||||
|
- **`create.ts` scaffolds a config without them**, asserted against the generated file.
|
||||||
|
- **`wrnexus update` still runs** with the pre-`0.8.0` migrations gone, and reports correctly for an
|
||||||
|
app already at the current version.
|
||||||
|
- **`examples/basic-app` builds and its suite passes** after its config is trimmed — the end-to-end
|
||||||
|
guard that nothing depended on the removed surface.
|
||||||
|
- **The full gate** (`bun run check:production`) passes, including the editor bundles, which embed
|
||||||
|
the compiler and must be rebuilt after `FunctionRuntime` changes.
|
||||||
|
|
||||||
|
## What we give up
|
||||||
|
|
||||||
|
Deleting `compatibilityDate` and `frameworkBehaviour` removes the standard escape hatch for changing
|
||||||
|
a default after going public — the mechanism that lets an existing app keep old behaviour by pinning
|
||||||
|
a date. Today it is wired to nothing, so it protects nobody, and an unused mechanism rots rather
|
||||||
|
than matures. If a gate is needed later it can be reintroduced deliberately, against a real
|
||||||
|
behaviour change, instead of being carried empty. This is a considered trade rather than a free
|
||||||
|
deletion.
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
# Typed, callable `api` blocks for `.wrn` files — Design
|
||||||
|
|
||||||
|
**Date:** 2026-08-19
|
||||||
|
**Status:** Approved for implementation
|
||||||
|
**Scope:** A sectioned `api` block that declares a typed request, transforms the response, and
|
||||||
|
handles failure — callable on demand from client code.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Calling this application's own API routes from a `.wrn` page should be declarative and
|
||||||
|
type-checked. Today it is neither: the `api` block takes no parameters at all, so anything
|
||||||
|
carrying a value from the page is written as a hand-rolled `fetch` — query-string assembly,
|
||||||
|
JSON headers, CSRF, status checks, and a `try/catch` repeated at every call site.
|
||||||
|
|
||||||
|
### What the current block cannot do
|
||||||
|
|
||||||
|
These are implementation facts, not gaps in documentation:
|
||||||
|
|
||||||
|
- **No query string.** `isSafeApiPath` (`packages/dev-server/src/runtime.ts`) rejects any path
|
||||||
|
containing `?` or `#`.
|
||||||
|
- **No interpolation.** `readPath()` reads until whitespace or `{`, so `/api/users?name={filter}`
|
||||||
|
ends the path at the brace and the remainder is parsed as the block body.
|
||||||
|
- **No request body.** The caller builds `new Request(apiUrl, { method, headers })` — there is no
|
||||||
|
parameter a payload could occupy, whatever method is named.
|
||||||
|
- **Fetch-once.** `setupCsrFetch` sets an `__wrnexusCsrFetch` flag and returns early on any later
|
||||||
|
pass, so a binding cannot be re-run.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- External or third-party APIs. Targets are restricted to this app's `/api/*` routes, preserving
|
||||||
|
the existing `isSafeApiPath` guarantee.
|
||||||
|
- Replacing `server function`. That remains the way to run arbitrary server logic over RPC.
|
||||||
|
- Author-settable headers. See "Why `headers` is excluded".
|
||||||
|
- Parameterised server-render fetching. See "The SSR boundary".
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
| Question | Decision |
|
||||||
|
| ---------------- | ------------------------------------------------------------------------ |
|
||||||
|
| Trigger | `client {}` blocks are callable on demand; `ssr {}` stays render-time |
|
||||||
|
| Targets | This app's `/api/*` routes only |
|
||||||
|
| Execution | Decided by the enclosing mode, not a modifier |
|
||||||
|
| Request values | Declared fields, supplied at the call site |
|
||||||
|
| Type source | Route contract when available, declared types otherwise (with a warning) |
|
||||||
|
| Type enforcement | `tsc`, via assertions generated into `wrnexus.generated.api-checks.ts` |
|
||||||
|
| Failure | `error {}` converts a failure to a value; without it, the call rejects |
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
client {
|
||||||
|
api searchUsers POST /api/users {
|
||||||
|
request {
|
||||||
|
body {
|
||||||
|
name?: string
|
||||||
|
age?: number
|
||||||
|
designation?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response {
|
||||||
|
return data.users
|
||||||
|
}
|
||||||
|
|
||||||
|
error {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Called as `const users = await api.searchUsers({ name: nameFilter.trim() })`. The `api` namespace
|
||||||
|
joins those already in client scope (`server`, `output`, `props`, `refs`), so it reads the same way
|
||||||
|
as `server.searchUsers()`.
|
||||||
|
|
||||||
|
`GET` blocks declare `parameters` rather than `body`; the compiler appends them as a query string at
|
||||||
|
call time. The path in source stays a plain literal, so `isSafeApiPath` is satisfied without
|
||||||
|
relaxing it.
|
||||||
|
|
||||||
|
### Backward compatibility
|
||||||
|
|
||||||
|
A bare body keeps meaning "this is the response block", unchanged:
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
ssr {
|
||||||
|
api ssrUsers GET /api/users/ssr {
|
||||||
|
return users.map((user) => user.name).join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The rule is **bare body = legacy untyped block; sections = typed block.**
|
||||||
|
|
||||||
|
The two forms reach the payload differently, and the reason is load-bearing rather than cosmetic.
|
||||||
|
The legacy form injects the response with `with ($data ?? {})`, which is why bare `users` resolves.
|
||||||
|
**`with` is untypeable** — TypeScript cannot see through it — so a typed `response` block is
|
||||||
|
impossible in that form. Sectioned blocks therefore bind the payload to `data`, specifically so
|
||||||
|
`tsc` can check `data.users` against the route's contract.
|
||||||
|
|
||||||
|
### Why `headers` is excluded
|
||||||
|
|
||||||
|
Own-route calls are same-origin, so cookies are already attached; `content-type` and `accept` follow
|
||||||
|
from whether the block has a body; and CSRF is attached by the runtime (below). What remains for an
|
||||||
|
author to set is mostly credentials, which do not belong in page source. Excluded from v1 pending a
|
||||||
|
concrete case.
|
||||||
|
|
||||||
|
## Type safety
|
||||||
|
|
||||||
|
### The constraint that shapes this
|
||||||
|
|
||||||
|
`examples/basic-app/tsconfig.json` uses `include: ["app"]` and excludes `.wrnexus-*`, and
|
||||||
|
`wrnexus build` never invokes `tsc`. **Generated build artifacts are not type-checked.** Compiling
|
||||||
|
the block into a typed client and expecting `tsc` to catch mismatches would therefore check nothing.
|
||||||
|
|
||||||
|
What _is_ type-checked is application source under `app/`. Enforcement goes there.
|
||||||
|
|
||||||
|
**Corrected 2026-08-19, during implementation.** This section originally placed the assertions in
|
||||||
|
`app/types/wrnexus.generated.d.ts`. That is inert: the root `tsconfig.json` sets
|
||||||
|
`skipLibCheck: true`, which exempts the _contents_ of every `.d.ts`, so an assertion written there
|
||||||
|
can never raise a `tsc` error. Proven by forcing `skipLibCheck: false`, under which the same
|
||||||
|
assertion fires as `TS2344`. The reasoning was right and the file was wrong. Per-block assertions
|
||||||
|
are emitted into a real `.ts` file instead — `app/types/wrnexus.generated.api-checks.ts` — which
|
||||||
|
`skipLibCheck` does not exempt and which `include: ["app"]` compiles. The helper types stay in the
|
||||||
|
`.d.ts`, where being declarations is correct.
|
||||||
|
|
||||||
|
### Three pieces
|
||||||
|
|
||||||
|
**1. Helper types**, extending what the generator already emits (`ApiRoute`, `ApiContracts`,
|
||||||
|
`ApiContract`):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type AssertAssignable<Actual, Expected> = Actual extends Expected ? true : never;
|
||||||
|
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
|
||||||
|
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Per-block assertions**, generated into `app/types/wrnexus.generated.api-checks.ts`. `wrnexus generate types` already parses
|
||||||
|
`.wrn` sources to build the route list, so it can read each block's declared fields and emit:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type __wrn_check_searchUsers = AssertAssignable<
|
||||||
|
{ name?: string; age?: number },
|
||||||
|
ApiInput<"/api/users", "POST">
|
||||||
|
>;
|
||||||
|
```
|
||||||
|
|
||||||
|
This is what makes the safety real. It sits in a file the project's own `tsc` already compiles, so
|
||||||
|
`bun run typecheck` fails when a block sends a field the endpoint rejects. No bespoke type
|
||||||
|
comparison inside the WRNexus compiler, and no need to type-check build output. The language server
|
||||||
|
already runs TypeScript diagnostics on `.wrn` documents, so the same error appears inline.
|
||||||
|
|
||||||
|
**3. A generic runtime**, `callApi(path, method, input)`, typed by those contracts, so the fetch,
|
||||||
|
JSON handling, and failure branch live in one tested place instead of being re-emitted per block.
|
||||||
|
|
||||||
|
### Routes without a contract
|
||||||
|
|
||||||
|
A plain handler returning `Response.json` has no `defineEndpoint` contract, so `ApiInput` resolves
|
||||||
|
to `unknown`. The block's declared types are used directly and the generator emits a warning naming
|
||||||
|
the route. Untyped endpoints stay visible rather than silently passing.
|
||||||
|
|
||||||
|
### GET parameters travel as strings
|
||||||
|
|
||||||
|
A `GET` block's `parameters` become a query string (see Request assembly below), and every
|
||||||
|
`URLSearchParams` value is text on the wire regardless of the declared field type — a block
|
||||||
|
declaring `age?: number` still sends and receives `"30"`, not `30`. The declared type is honest
|
||||||
|
only because the endpoint's own schema coerces it back: `checkField` in
|
||||||
|
`packages/validation/src/index.ts` calls `Number(pre)` for every `v.number()` field — optional or
|
||||||
|
required — before the handler ever sees it, so `defineEndpoint({ input: v.object({ age:
|
||||||
|
v.number() }) })` invoked as `?age=30` hands the handler an actual `number` (verified end to end;
|
||||||
|
regression-tested in `packages/core/test/endpoint-schema.test.ts`, "a GET request coerces a
|
||||||
|
v.number() query param to an actual number"). This is a property of the endpoint's schema, not of
|
||||||
|
the `api` block or the generated contract types — a route that reads `ctx.url.searchParams`
|
||||||
|
directly, with no `defineEndpoint` schema, receives raw strings and gets no coercion, but that
|
||||||
|
route also has no contract for the generator to check against, so it already falls under "Routes
|
||||||
|
without a contract" above and is flagged there.
|
||||||
|
|
||||||
|
### Staleness
|
||||||
|
|
||||||
|
Checking is only as current as the generated file, so this stays wired into the existing
|
||||||
|
`check:generated-types` gate, which already verifies those artifacts match their sources.
|
||||||
|
|
||||||
|
## Compilation and runtime
|
||||||
|
|
||||||
|
### Client mode
|
||||||
|
|
||||||
|
Each `client { api name ... }` becomes an entry on an `api` namespace in the generated browser
|
||||||
|
module, beside the existing `__wrnexusClientFunctions`, with `const api = context.api` added to
|
||||||
|
client scope exactly as `server` is today.
|
||||||
|
|
||||||
|
Declared field types are **type-only**. The generator uses them for the `.d.ts` assertions and
|
||||||
|
codegen drops them before emit. A client function body that carried TypeScript into a `.mjs`
|
||||||
|
artifact is a bug this repository has already shipped once (fixed 2026-08-19, `55fed217`); the same
|
||||||
|
discipline applies here.
|
||||||
|
|
||||||
|
`setupCsrFetch` is untouched. Callable blocks are a separate mechanism, so the existing render-time
|
||||||
|
binding needs no rework.
|
||||||
|
|
||||||
|
### Request assembly
|
||||||
|
|
||||||
|
`callApi` builds the request:
|
||||||
|
|
||||||
|
- **GET** — declared `parameters` become a query string; `undefined` fields are omitted, which
|
||||||
|
removes the `if (filter.trim())` ladder authors write by hand.
|
||||||
|
- **Everything else** — a JSON body with `content-type: application/json`.
|
||||||
|
- Always `credentials: "same-origin"` and `accept: application/json`.
|
||||||
|
- **Non-GET requests attach `x-csrf-token`**, read from the `wrn-csrf` cookie or the
|
||||||
|
`wrnexus-csrf` meta tag, reusing the logic already at `packages/csr/src/reactive-runtime.ts:4221`
|
||||||
|
for RPC. Hand-written `fetch` calls in application code generally omit this, so it is a
|
||||||
|
correctness gain rather than only less typing.
|
||||||
|
|
||||||
|
### Failure
|
||||||
|
|
||||||
|
**`error {}` converts a failure into a value; without it, the call rejects.**
|
||||||
|
|
||||||
|
- 2xx — the JSON is parsed and bound as `data`, `response {}` runs, and its return value is the
|
||||||
|
call's result. With no `response` block, `data` is returned unchanged.
|
||||||
|
- Non-2xx, network failure, or an unparseable body — `error {}` runs with `status`, `message`, and
|
||||||
|
`data` in scope. `return []` yields an empty list and no exception.
|
||||||
|
- No `error {}` block — the promise rejects, so `try/catch` at the call site keeps working.
|
||||||
|
|
||||||
|
A block must never quietly return `undefined` on failure. Success-shaped failure is the defect class
|
||||||
|
this design is most concerned with, so the absence of an `error` block means throw, never swallow.
|
||||||
|
|
||||||
|
### The SSR boundary
|
||||||
|
|
||||||
|
`ssr {}` blocks accept `response {}` and `error {}`, but **not** `request {}`. There is no caller at
|
||||||
|
render time to supply arguments, and inferring an implicit source — page state, query parameters —
|
||||||
|
would be a guess. Parameterised requests are a client-mode feature; parameterised server-side
|
||||||
|
fetching stays with `server function`.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Parser (`packages/syntax/test/`)
|
||||||
|
|
||||||
|
- A sectioned block parses into `request`/`response`/`error` parts.
|
||||||
|
- A bare body still parses as the response block (the backward-compatibility guarantee).
|
||||||
|
- `request` inside an `ssr {}` block is a parse error naming the restriction.
|
||||||
|
- A malformed section reports the offending offset rather than failing later in codegen.
|
||||||
|
|
||||||
|
### Type generation (`packages/cli/test/` or the types generator's suite)
|
||||||
|
|
||||||
|
- A block targeting a `defineEndpoint` route emits an assertion referencing that contract.
|
||||||
|
- A field the endpoint does not accept makes `bun run typecheck` fail — asserted by running `tsc`
|
||||||
|
over a fixture, not by string-matching the generated file.
|
||||||
|
- A block targeting a contract-less route emits the warning and falls back to declared types.
|
||||||
|
- `check:generated-types` still passes with blocks present.
|
||||||
|
|
||||||
|
### Codegen (`packages/compiler/test/`)
|
||||||
|
|
||||||
|
- A client-mode block emits an `api` namespace entry and valid JavaScript — no TypeScript survives
|
||||||
|
into the browser module (the guard for the `55fed217` defect class).
|
||||||
|
- Declared types do not appear in the emitted module.
|
||||||
|
- An `ssr` block's output is unchanged from today for a bare body.
|
||||||
|
|
||||||
|
### Runtime (`packages/csr/test/`)
|
||||||
|
|
||||||
|
- GET omits `undefined` parameters and includes the rest.
|
||||||
|
- Non-GET attaches `x-csrf-token` from cookie and from meta.
|
||||||
|
- 2xx runs `response`; its return value is the result.
|
||||||
|
- Non-2xx runs `error`; its return value is the result.
|
||||||
|
- With no `error` block, a non-2xx rejects rather than resolving to `undefined`.
|
||||||
|
|
||||||
|
### End to end (`examples/basic-app`)
|
||||||
|
|
||||||
|
A page calling a typed block against a real route, driven in a browser: the request carries the
|
||||||
|
declared fields, the response block's value reaches page state, and a deliberately failing call
|
||||||
|
takes the `error` path. Tests that pass while the feature does not work have been a recurring
|
||||||
|
failure in this repository, so browser verification is part of the definition of done.
|
||||||
|
|
||||||
|
## Deferred
|
||||||
|
|
||||||
|
- External and third-party API targets, with the allowlist and credential handling they require.
|
||||||
|
- Author-settable request headers.
|
||||||
|
- Re-runnable `ssr` bindings — `setupCsrFetch`'s fetch-once guard stays as it is.
|
||||||
|
- Parameterised server-render fetching.
|
||||||
|
- Response caching and request de-duplication.
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Carrying projects to the new syntax with `wrnexus update` — Design
|
||||||
|
|
||||||
|
**Date:** 2026-08-19
|
||||||
|
**Status:** Approved for implementation
|
||||||
|
**Scope:** Migrations that take an existing project from today's syntax to the syntax left by the
|
||||||
|
cleanup and `apis { }` specs.
|
||||||
|
|
||||||
|
**Depends on:** `2026-08-19-legacy-and-config-cleanup-design.md` and
|
||||||
|
`2026-08-19-apis-block-design.md`. Both define the target this migrates to, so both must land first.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
After the two preceding specs, every existing project is written in a syntax the framework no longer
|
||||||
|
accepts. One `wrnexus update` should carry a project across — config keys removed, `api` blocks
|
||||||
|
moved into `apis { }`, mode-scoped helpers relocated — and, where it cannot do that safely, say so
|
||||||
|
precisely instead of guessing.
|
||||||
|
|
||||||
|
## What already exists
|
||||||
|
|
||||||
|
This is an extension of working machinery, not a new subsystem:
|
||||||
|
|
||||||
|
- `Migration { version, id, description, apply(ctx) }`, run when `from < version <= to`.
|
||||||
|
- `MigrationCtx` carries `appRoot`, `from`, `to`, **`dryRun`**, a report, and a logger.
|
||||||
|
- `MigrationReport` already separates `changedAutomatically`, `needsReview`, `parseFailures`,
|
||||||
|
`ambiguousFunctions`, and `unresolvedImports`.
|
||||||
|
- `update.ts` already imports `parse` and `formatWrn` from `@wrnexus/syntax`, so parsing a `.wrn`
|
||||||
|
file, transforming it, and re-emitting formatted source is an established pattern here.
|
||||||
|
|
||||||
|
The report's shape matters: it was built around the idea that some changes are safe to make and
|
||||||
|
others must be handed back to a human. That distinction is the backbone of this spec.
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Migrating projects below `0.8.0`. The cleanup spec removes those migrations; no such project
|
||||||
|
exists.
|
||||||
|
- Rewriting application logic. Only the constructs these specs changed.
|
||||||
|
|
||||||
|
## The safety contract
|
||||||
|
|
||||||
|
**A file is transformed correctly, or it is left untouched and reported.** There is no third
|
||||||
|
outcome. Concretely:
|
||||||
|
|
||||||
|
1. Parse the file. A parse failure records the path in `parseFailures` and moves on — the file is
|
||||||
|
never partially rewritten.
|
||||||
|
2. Transform, then re-emit through `formatWrn`.
|
||||||
|
3. If any part of a file's transform cannot be completed, **the whole file is skipped** and recorded
|
||||||
|
in `needsReview` with the reason and the construct involved.
|
||||||
|
|
||||||
|
Running the migration twice must be a no-op: every transform detects already-migrated input and
|
||||||
|
does nothing. Dry-run must report exactly what a real run would change.
|
||||||
|
|
||||||
|
## The migrations
|
||||||
|
|
||||||
|
### 1. Remove the dead config keys
|
||||||
|
|
||||||
|
Delete `compatibility`, `functions`, `compatibilityDate`, and `frameworkBehaviour` from
|
||||||
|
`wrnexus.config.ts`. Mechanical, fully automatic, and safe because none of them was ever read.
|
||||||
|
|
||||||
|
### 2. `ssr { api … }` / `client { api … }` → `apis { … }`
|
||||||
|
|
||||||
|
Move each `api` entry into a page-level `apis { }` block, dropping the mode. Sectioned bodies —
|
||||||
|
those already using `request` / `response` / `error` — carry across unchanged, because the payload
|
||||||
|
is already bound to `data`.
|
||||||
|
|
||||||
|
Fully automatic. If a page has entries in both an `ssr` and a `client` block sharing a name, that is
|
||||||
|
a duplicate under the new rules and the **file is skipped and reported**, since choosing which one
|
||||||
|
survives is a decision about intent.
|
||||||
|
|
||||||
|
### 3. `ssr { functions { … } }` → `functions { shared function … }`
|
||||||
|
|
||||||
|
Relocate mode-scoped helpers to the page-level `functions { }` block with the `shared` modifier.
|
||||||
|
Automatic. If the page already has a function of the same name, the file is skipped and reported.
|
||||||
|
|
||||||
|
### 4. Legacy bare-body `api` blocks — **needs review, not automatic**
|
||||||
|
|
||||||
|
This is the one transform that cannot be done safely, and the spec is explicit about it rather than
|
||||||
|
attempting a best effort.
|
||||||
|
|
||||||
|
A legacy bare body is evaluated inside `with ($data ?? {})`, so it references payload fields as bare
|
||||||
|
identifiers:
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
api ssrUsers GET /api/users/ssr {
|
||||||
|
return userNames(users)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The sectioned form binds the payload to `data`, so this must become `data.users`. But **which free
|
||||||
|
identifiers are payload fields is not knowable from the source.** In the example, `users` comes from
|
||||||
|
the response and `userNames` is a page helper — and nothing in the file distinguishes them. The
|
||||||
|
response shape belongs to the route, and the route may not even be typed.
|
||||||
|
|
||||||
|
A migration that guessed would produce code that compiles and is wrong: `data.userNames(...)` or an
|
||||||
|
untouched `users` that silently resolves to `undefined`. That is precisely the silent-wrong-answer
|
||||||
|
failure this project keeps paying for.
|
||||||
|
|
||||||
|
So: legacy bare-body blocks are **detected, reported in `needsReview` with the file, the block name,
|
||||||
|
and the free identifiers found**, and left untouched. The report tells the author exactly what to
|
||||||
|
decide. `wrnexus update` prints a short explanation of why this one is manual.
|
||||||
|
|
||||||
|
### 5. Deprecated `@wrnexus/auth` options
|
||||||
|
|
||||||
|
Only if the cleanup spec's optional auth section is included. Rename call sites of the superseded
|
||||||
|
options. Automatic where the rename is unambiguous; reported otherwise.
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
All of these attach to the release that ships the breaking change. After the cleanup spec the
|
||||||
|
migration floor is `0.8.0`, so the list is short and every entry is reachable.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
At the end of a run the command prints, in this order: what it changed, what needs review and why,
|
||||||
|
and what failed to parse. A run with anything in `needsReview` or `parseFailures` exits non-zero, so
|
||||||
|
a scripted upgrade cannot appear to succeed while leaving a project half-migrated.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Each migration gets a fixture project and three assertions: the transform produces the expected
|
||||||
|
source, running it a second time changes nothing, and a dry run reports the same set without writing.
|
||||||
|
|
||||||
|
- **Config removal** — keys gone, rest of the config untouched.
|
||||||
|
- **`api` relocation** — a page with both `ssr` and `client` api blocks lands in one `apis { }`;
|
||||||
|
entries keep their names, methods, paths, and sections.
|
||||||
|
- **Name collision across modes** — the file is skipped and reported, not silently merged.
|
||||||
|
- **Mode functions** — relocated with the `shared` modifier; a name collision skips and reports.
|
||||||
|
- **Legacy bare body** — reported in `needsReview` with the block name and free identifiers, and the
|
||||||
|
file is byte-identical afterwards. This is the most important test in the spec: it pins that the
|
||||||
|
migration does _not_ attempt the rewrite.
|
||||||
|
- **Parse failure** — a malformed `.wrn` is recorded in `parseFailures` and left untouched.
|
||||||
|
- **Exit code** — non-zero when anything needs review.
|
||||||
|
- **End to end** — `examples/basic-app` migrated by the command alone, then built and tested. If the
|
||||||
|
framework's own example cannot be migrated by the tool, the tool is not finished.
|
||||||
|
|
||||||
|
## What this does not promise
|
||||||
|
|
||||||
|
Automated source rewriting cannot be promised as "perfect". What is promised is bounded: every file
|
||||||
|
is either correctly transformed or untouched and named in the report, with the reason. Nothing is
|
||||||
|
half-rewritten, and nothing is guessed. The legacy bare-body case is deliberately manual because a
|
||||||
|
correct automatic answer does not exist.
|
||||||
@@ -4,16 +4,16 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b",
|
"packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b",
|
||||||
"packages/ui/components/AdvancedSelect.wrn": "b952b894a5b35050fc2f6e43c4182101eeee05c75662e2aca4abfe61512b305b",
|
"packages/ui/components/AdvancedSelect.wrn": "b952b894a5b35050fc2f6e43c4182101eeee05c75662e2aca4abfe61512b305b",
|
||||||
"packages/ui/components/AnnouncementBar.wrn": "cb66b8d0619ee798a115b43f0ad93fda93dfb46571589e7a9f0a8d65ec3c84c9",
|
"packages/ui/components/AnnouncementBar.wrn": "5b3219db555e0fcdb94ec41dc18186ee1b7c0cf9c260469f7fb1fc43435c28c4",
|
||||||
"packages/ui/components/AuthForm.wrn": "46a3e1db1c9e69efc52473cbf2fababdcbfe7144e7571469638d2c3addd4d7f0",
|
"packages/ui/components/AuthForm.wrn": "46a3e1db1c9e69efc52473cbf2fababdcbfe7144e7571469638d2c3addd4d7f0",
|
||||||
"packages/ui/components/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5",
|
"packages/ui/components/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5",
|
||||||
"packages/ui/components/AvatarGroup.wrn": "30b702e06a1adff8b9bae32820206020e086a69560cbdcbadb19f04f25951f97",
|
"packages/ui/components/AvatarGroup.wrn": "30b702e06a1adff8b9bae32820206020e086a69560cbdcbadb19f04f25951f97",
|
||||||
"packages/ui/components/BackToTop.wrn": "5b760ec7941ffd8e58c5dedae1b322ee46e072e927533d7ec077b511a816e436",
|
"packages/ui/components/BackToTop.wrn": "d4b8d1fd37ff26c89b4276ce54ce6ac9581dc2ff8bbcbe160e072778bba76eab",
|
||||||
"packages/ui/components/Blockquote.wrn": "3761bf13babb6eb07ce7392258ee439ef5478064cf589eff63f945b950e00d2e",
|
"packages/ui/components/Blockquote.wrn": "3761bf13babb6eb07ce7392258ee439ef5478064cf589eff63f945b950e00d2e",
|
||||||
"packages/ui/components/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a",
|
"packages/ui/components/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a",
|
||||||
"packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72",
|
"packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72",
|
||||||
"packages/ui/components/CTASection.wrn": "d0665f2aa33eea95e6f84120f739b3dd9c8723c882138bcb1a7f80dcd7909904",
|
"packages/ui/components/CTASection.wrn": "f6872b980432d5ee9d88388250be9838ccce51bd706349b283ef472ee692b237",
|
||||||
"packages/ui/components/Card.wrn": "3cc2ff238bda279c169026a462236144ad11ec0046deef00ab00e001a07e8a4c",
|
"packages/ui/components/Card.wrn": "be8cd6a438d1ede4c284a302b76f0fbc235a2423b83a7ced21131763fecc8a90",
|
||||||
"packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9",
|
"packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9",
|
||||||
"packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4",
|
"packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4",
|
||||||
"packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1",
|
"packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1",
|
||||||
@@ -36,10 +36,10 @@
|
|||||||
"packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0",
|
"packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0",
|
||||||
"packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced",
|
"packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced",
|
||||||
"packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897",
|
"packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897",
|
||||||
"packages/ui/components/FeatureIconCard.wrn": "3b4f3fa62c6729e686886a5b848e26c255766684829cec2b715967b4e73d6f07",
|
"packages/ui/components/FeatureIconCard.wrn": "5f65d64235babc3e653a5695204412202015e6f3ffadca566fc18c7f9ce70428",
|
||||||
"packages/ui/components/FileInput.wrn": "866a292a3280527bf429893469e7c50f7cf38965d8adb37b45a35ede1606d5b8",
|
"packages/ui/components/FileInput.wrn": "866a292a3280527bf429893469e7c50f7cf38965d8adb37b45a35ede1606d5b8",
|
||||||
"packages/ui/components/FileUploadProgress.wrn": "e5da29c562a521cdd6bb50b8f4d217aa1ab981d7b2a2432af47391472f0a8034",
|
"packages/ui/components/FileUploadProgress.wrn": "e5da29c562a521cdd6bb50b8f4d217aa1ab981d7b2a2432af47391472f0a8034",
|
||||||
"packages/ui/components/Footer.wrn": "f7c5a77064a09e244f689d42475c4c20143f4c8859cac451c10a50c14652db33",
|
"packages/ui/components/Footer.wrn": "e3515e05ef3ef51e6dad71d67ddfe110c80fdcf69e952aecdc7973e69d4f08c7",
|
||||||
"packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517",
|
"packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517",
|
||||||
"packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900",
|
"packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900",
|
||||||
"packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790",
|
"packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790",
|
||||||
@@ -55,30 +55,30 @@
|
|||||||
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
|
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
|
||||||
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
|
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
|
||||||
"packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b",
|
"packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b",
|
||||||
"packages/ui/components/MegaMenu.wrn": "6a6db166259f8f2bd7ac8fbb21dba6d915c4b936d87b19805caa78189f54730a",
|
"packages/ui/components/MegaMenu.wrn": "38877d3ff59e573fffa45d7d1b101cd41495e8f7db66c6528ec0ac3969677d60",
|
||||||
"packages/ui/components/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5",
|
"packages/ui/components/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5",
|
||||||
"packages/ui/components/MetricGrid.wrn": "c71e53249835908833055b553e64a8ee55fdf11ac4605436a9581ebb87a0608b",
|
"packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
|
||||||
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
|
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
|
||||||
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
|
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
|
||||||
"packages/ui/components/Navbar.wrn": "01903230210cd2e5e6d9325a0708f85af9623d4118e3a53f1adb770fd545425d",
|
"packages/ui/components/Navbar.wrn": "a3cf984193e28c692894031417df74ebca66edeaa650c39e38b155aa54193075",
|
||||||
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
|
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
|
||||||
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
|
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
|
||||||
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
|
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
|
||||||
"packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019",
|
"packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019",
|
||||||
"packages/ui/components/PortalDashboard.wrn": "f04171eb1f8cabef06de3c20e4694b766def3407e1e20d2c9aaebc9bb7da03ee",
|
"packages/ui/components/PortalDashboard.wrn": "f04171eb1f8cabef06de3c20e4694b766def3407e1e20d2c9aaebc9bb7da03ee",
|
||||||
"packages/ui/components/PreferenceSwitcher.wrn": "77229e15449182563ebf49159507139928eb74e799e4438e00e9aa81fd60f982",
|
"packages/ui/components/PreferenceSwitcher.wrn": "519cf911390f529fff3fd16e11fb58e79d618fe851902202dc37552ee1182395",
|
||||||
"packages/ui/components/PublicPageShell.wrn": "3d3f25c03d48f3d3d7e3c567e79cb137c7fce1867e054fd6d440fa1d2e961077",
|
"packages/ui/components/PublicPageShell.wrn": "3d3f25c03d48f3d3d7e3c567e79cb137c7fce1867e054fd6d440fa1d2e961077",
|
||||||
"packages/ui/components/RangeSlider.wrn": "bbf646c5d5a89a62986290db811d77b8f635ebcaea9ca2d334b569aa5edc785b",
|
"packages/ui/components/RangeSlider.wrn": "bbf646c5d5a89a62986290db811d77b8f635ebcaea9ca2d334b569aa5edc785b",
|
||||||
"packages/ui/components/Rating.wrn": "902b55c64e3798f3097f09b1558d1a2372f66d4f760662de085783108ee04587",
|
"packages/ui/components/Rating.wrn": "902b55c64e3798f3097f09b1558d1a2372f66d4f760662de085783108ee04587",
|
||||||
"packages/ui/components/Scrollspy.wrn": "b9c6400ee92a7c228362610a2051f37bff1d3baba1dbacd66d58397dfda5d856",
|
"packages/ui/components/Scrollspy.wrn": "4b452fcbd524feda9c9f2c81894ee01747a51888e3b86fafb37c5d4d192a3a51",
|
||||||
"packages/ui/components/SearchBox.wrn": "3f88340e0b69c0cd552023c72bd22dc511bc00422d46060097bcf0777e683675",
|
"packages/ui/components/SearchBox.wrn": "3f88340e0b69c0cd552023c72bd22dc511bc00422d46060097bcf0777e683675",
|
||||||
"packages/ui/components/Section.wrn": "45b7a1977c25a6020d5cc8bab2ce9c80730e7ec2d51ad53e8c9f64cd9b5b64cd",
|
"packages/ui/components/Section.wrn": "78da13eb8bc1acafe8e6151cbaeed4baaac2ab860e3209f1d13c467f279d2e68",
|
||||||
"packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5",
|
"packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5",
|
||||||
"packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6",
|
"packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6",
|
||||||
"packages/ui/components/SplitHero.wrn": "843387dc7b920b3cf6a1b2b785b8653c281982e524479edc2638a89381ff3577",
|
"packages/ui/components/SplitHero.wrn": "7127cb869d1228522d066e028818dd9e512de8b6dd8f99e3dec500e6da5a3cb0",
|
||||||
"packages/ui/components/StatsBar.wrn": "c273bd6ca417a0a20e5e6f1214414f678741b73bedd885fea1037575fa8c5b95",
|
"packages/ui/components/StatsBar.wrn": "6fabcb4374b1e074f58ebcaba968f235c8a3409762163aea51298e2515a14435",
|
||||||
"packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142",
|
"packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142",
|
||||||
"packages/ui/components/StrongPassword.wrn": "d5ea906fafe268b070a26ab551a252c0575d899c9d2b03b337bd047688b85dc5",
|
"packages/ui/components/StrongPassword.wrn": "407eabcc13affc9323a346ddba8e39c81b885a59ef7043d2bd5567d4b7553282",
|
||||||
"packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d",
|
"packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d",
|
||||||
"packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9",
|
"packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9",
|
||||||
"packages/ui/components/TextLink.wrn": "9d561631c5f65a2bcdc59f5c0b166a5dd794a396f42093f2cd26388386f6f954",
|
"packages/ui/components/TextLink.wrn": "9d561631c5f65a2bcdc59f5c0b166a5dd794a396f42093f2cd26388386f6f954",
|
||||||
@@ -86,14 +86,14 @@
|
|||||||
"packages/ui/components/Timeline.wrn": "fb553f69763840532eef1c0aafa79013f57411dc5335e7cb7e7c20c5a3753008",
|
"packages/ui/components/Timeline.wrn": "fb553f69763840532eef1c0aafa79013f57411dc5335e7cb7e7c20c5a3753008",
|
||||||
"packages/ui/components/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c",
|
"packages/ui/components/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c",
|
||||||
"packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f",
|
"packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f",
|
||||||
"packages/ui/components/TogglePassword.wrn": "b6176453cb2c586fe88c3db6bdd734ae2b4c95db46d57e0de298dfba3d0dd0c2",
|
"packages/ui/components/TogglePassword.wrn": "6bd4d9fa82b0c8e80178430d5a42b1b3ec9725fb62be158585f68af9338c901a",
|
||||||
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
|
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
|
||||||
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
|
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
|
||||||
"packages/ui/components/WysiwygEditor.wrn": "3f9d1765184581e3c1f335769a9f19969917450258bc95ba076980d99f5610b5",
|
"packages/ui/components/WysiwygEditor.wrn": "a858ebd4e6e2672463ba57d4d049176ca1cc98dac32ab3df3638d6a9cdcd0f2d",
|
||||||
"packages/ui/components/alert.wrn": "d42287e41b918b1e19962e9462ef203002eb5480acac984f115b711e9dbe663d",
|
"packages/ui/components/alert.wrn": "d42287e41b918b1e19962e9462ef203002eb5480acac984f115b711e9dbe663d",
|
||||||
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
|
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
|
||||||
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
|
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
|
||||||
"packages/ui/components/button.wrn": "cba4ccbbd23bb75b3836ec7a53673e40f1e043d18bfcee3d613db96c318f4ba8",
|
"packages/ui/components/button.wrn": "65570c58cb2547f1e17cd158eec7cf7b1ffc721e6976c6486c9df564627b1219",
|
||||||
"packages/ui/components/checkbox.wrn": "18046396b75d0b9c6bb2bdb09dbff1846c84fb07490f390a25ca7b5367fd2ef4",
|
"packages/ui/components/checkbox.wrn": "18046396b75d0b9c6bb2bdb09dbff1846c84fb07490f390a25ca7b5367fd2ef4",
|
||||||
"packages/ui/components/input.wrn": "e4c14527f009b610b4aa96d8d7b7d5ac3ca1ffd34237bab348ee9dc4ddc55847",
|
"packages/ui/components/input.wrn": "e4c14527f009b610b4aa96d8d7b7d5ac3ca1ffd34237bab348ee9dc4ddc55847",
|
||||||
"packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492",
|
"packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492",
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.8.9
|
||||||
|
|
||||||
|
- Added highlighting for the page-level `apis { }` block and its `<name> <METHOD> <path>` entries,
|
||||||
|
along with container and entry completion snippets.
|
||||||
|
- Added `api.` call completion listing every declared block with its method and path, and hover
|
||||||
|
reporting a block's method, path, and request fields.
|
||||||
|
- Taught the language server the `api="…"` binding attribute: it is no longer treated as unknown,
|
||||||
|
and completion inside its quotes offers the page's block names.
|
||||||
|
- Diagnostics for the removed `ssr { … }` / `client { … }` data blocks now name the `apis { }`
|
||||||
|
replacement and land on the offending keyword instead of the start of the file.
|
||||||
|
- Fixed `api.<name>()` calls reporting a spurious "Cannot find name 'api'" error, and the implicit
|
||||||
|
`any` that followed from it.
|
||||||
|
- Restricted API hover to real `api.<name>` references, so a local variable that merely shares a
|
||||||
|
block's name keeps its own hover information.
|
||||||
|
- `client state { }`, `runtime = "client"`, and the `client function` modifier are unaffected —
|
||||||
|
only the `ssr { }` / `client { }` data-block form was removed.
|
||||||
|
|
||||||
|
## 0.8.8
|
||||||
|
|
||||||
|
- Added HTML tag and attribute completions inside WRN `view` blocks, while preserving WRNexus
|
||||||
|
component completion priority and suppressing HTML suggestions outside markup regions.
|
||||||
|
- Added HTML hover documentation, folding ranges, linked tag editing, and automatic closing tags.
|
||||||
|
- Added Emmet expansion support for WRN documents and kept void elements from receiving closing tags.
|
||||||
|
- Hardened completion and auto-close handling against quoted attribute values, replaced selections,
|
||||||
|
stale asynchronous edits, and duplicate client-side suggestions.
|
||||||
|
|
||||||
## 0.8.3
|
## 0.8.3
|
||||||
|
|
||||||
- Rebuilt the embedded WRN compiler with the 0.8.3 SSR, computed-value, Async-scope, and typed loop-prop fixes.
|
- Rebuilt the embedded WRN compiler with the 0.8.3 SSR, computed-value, Async-scope, and typed loop-prop fixes.
|
||||||
@@ -16,8 +42,8 @@
|
|||||||
- Kept component prop/event intelligence active while the shared language server is enabled.
|
- Kept component prop/event intelligence active while the shared language server is enabled.
|
||||||
- Suppressed unavailable TypeScript standard-library and unmapped virtual-document implementation
|
- Suppressed unavailable TypeScript standard-library and unmapped virtual-document implementation
|
||||||
diagnostics in packaged extension environments.
|
diagnostics in packaged extension environments.
|
||||||
- Fixed false `unknown`/index-access diagnostics in valid dynamic event forwarding handlers such as
|
- Fixed false `unknown`/index-access diagnostics in valid dynamic event forwarding handlers that
|
||||||
`output[type](payload)` by preserving JavaScript semantics for omitted parameter types.
|
dispatch a payload by event type, preserving JavaScript semantics for omitted parameter types.
|
||||||
- Resolved TypeScript standard libraries from the active workspace so semantic diagnostics run
|
- Resolved TypeScript standard libraries from the active workspace so semantic diagnostics run
|
||||||
consistently in the repository and extension development environment.
|
consistently in the repository and extension development environment.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"displayName": "WRNexus Language Support",
|
"displayName": "WRNexus Language Support",
|
||||||
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
|
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
|
||||||
"version": "0.8.8",
|
"version": "0.8.10",
|
||||||
"publisher": "wrnexus",
|
"publisher": "wrnexus",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
@@ -134,6 +134,11 @@
|
|||||||
"maximum": 240,
|
"maximum": 240,
|
||||||
"scope": "resource",
|
"scope": "resource",
|
||||||
"description": "Preferred WRNexus formatter line width before long tags are expanded."
|
"description": "Preferred WRNexus formatter line width before long tags are expanded."
|
||||||
|
},
|
||||||
|
"wrnexus.html.autoClosingTags": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"description": "Automatically close HTML tags inside .wrn view blocks."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -141,6 +146,9 @@
|
|||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.wrn": "wrn"
|
"*.wrn": "wrn"
|
||||||
},
|
},
|
||||||
|
"emmet.includeLanguages": {
|
||||||
|
"wrn": "html"
|
||||||
|
},
|
||||||
"[wrn]": {
|
"[wrn]": {
|
||||||
"editor.defaultFormatter": "wrnexus.wrnexus",
|
"editor.defaultFormatter": "wrnexus.wrnexus",
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
@@ -276,13 +284,14 @@
|
|||||||
"check": "bun run build && bun run test && bun run validate",
|
"check": "bun run build && bun run test && bun run validate",
|
||||||
"vscode:prepublish": "bun run check",
|
"vscode:prepublish": "bun run check",
|
||||||
"package": "vsce package --no-dependencies --no-rewrite-relative-links",
|
"package": "vsce package --no-dependencies --no-rewrite-relative-links",
|
||||||
"publish": "vsce publish --no-dependencies --no-rewrite-relative-links",
|
"publish": "vsce publish --no-dependencies",
|
||||||
"publish:azure": "vsce publish --no-dependencies --no-rewrite-relative-links --azure-credential"
|
"publish:azure": "vsce publish --no-dependencies --azure-credential"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vscode/vsce": "^3.9.2"
|
"@vscode/vsce": "^3.9.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vscode-languageclient": "^10.1.0"
|
"vscode-languageclient": "^10.1.0",
|
||||||
|
"vscode-html-languageservice": "^5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const vscode = require("vscode");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-close tags as they are typed.
|
||||||
|
*
|
||||||
|
* LSP has no request for this, so the client watches document changes and asks
|
||||||
|
* the server whether the tag should close. The server owns the decision because
|
||||||
|
* void elements and already-closed tags must not be closed.
|
||||||
|
*/
|
||||||
|
function registerAutoCloseTags(context, client) {
|
||||||
|
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
|
||||||
|
if (event.document.languageId !== "wrn") return;
|
||||||
|
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true)) return;
|
||||||
|
|
||||||
|
const changes = event.contentChanges;
|
||||||
|
if (!changes.length) return;
|
||||||
|
|
||||||
|
const typed = changes[0].text;
|
||||||
|
if (typed !== ">" && typed !== "/") return;
|
||||||
|
// Every cursor must have typed the same trigger. A replaced selection
|
||||||
|
// (overtype, or select-and-type) is declined rather than guessed at.
|
||||||
|
if (!changes.every((change) => change.text === typed && change.rangeLength === 0)) return;
|
||||||
|
|
||||||
|
const editor = vscode.window.activeTextEditor;
|
||||||
|
if (!editor || editor.document !== event.document) return;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Positions come from the editor's selections, not from the changes.
|
||||||
|
*
|
||||||
|
* A change's `range` is in coordinates from before the whole event, so with
|
||||||
|
* several cursors on one line every range after the first is short by the
|
||||||
|
* insertions preceding it. The selections have already been adjusted for
|
||||||
|
* the edit, so they are where the carets actually are.
|
||||||
|
*/
|
||||||
|
const positions = editor.selections.map((selection) => selection.active);
|
||||||
|
if (positions.length !== changes.length) return;
|
||||||
|
if (!editor.selections.every((selection) => selection.isEmpty)) return;
|
||||||
|
|
||||||
|
const documentVersion = event.document.version;
|
||||||
|
const snippets = await Promise.all(
|
||||||
|
positions.map((position) =>
|
||||||
|
client.sendRequest("wrn/tagComplete", {
|
||||||
|
textDocument: { uri: event.document.uri.toString() },
|
||||||
|
position: { line: position.line, character: position.character },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!snippets.every((snippet) => typeof snippet === "string" && snippet)) return;
|
||||||
|
/*
|
||||||
|
* One insertSnippet call carries one snippet, and it is the only form that
|
||||||
|
* keeps every caret: inserting sequentially would collapse the selection to
|
||||||
|
* the first snippet and invalidate the remaining positions. Cursors that
|
||||||
|
* want different closing tags are therefore declined rather than
|
||||||
|
* half-applied -- multi-cursor editing of matching lines, which is what
|
||||||
|
* this is for, produces one snippet for all of them.
|
||||||
|
*/
|
||||||
|
if (!snippets.every((snippet) => snippet === snippets[0])) return;
|
||||||
|
|
||||||
|
// The user may have kept typing during the round-trip; re-validate everything the
|
||||||
|
// insertion depends on before touching the document, since a stale offset would
|
||||||
|
// silently corrupt it.
|
||||||
|
if (vscode.window.activeTextEditor !== editor) return;
|
||||||
|
if (editor.document !== event.document) return;
|
||||||
|
if (editor.document.version !== documentVersion) return;
|
||||||
|
if (editor.selections.length !== positions.length) return;
|
||||||
|
if (
|
||||||
|
!editor.selections.every(
|
||||||
|
(selection, index) => selection.isEmpty && selection.active.isEqual(positions[index]),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await editor.insertSnippet(new vscode.SnippetString(snippets[0]), positions);
|
||||||
|
});
|
||||||
|
|
||||||
|
context.subscriptions.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { registerAutoCloseTags };
|
||||||
+1010
-261
File diff suppressed because it is too large
Load Diff
@@ -306,6 +306,54 @@ const BLOCK_COMPLETIONS = [
|
|||||||
"}",
|
"}",
|
||||||
].join("\n"),
|
].join("\n"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "apis",
|
||||||
|
detail: "Page-level API call declarations block",
|
||||||
|
documentation:
|
||||||
|
"Declare the API calls available to this page as named entries, each callable via api.<name>() from functions and the `api=` view binding.",
|
||||||
|
snippet: [
|
||||||
|
"apis {",
|
||||||
|
" ${1:searchUsers} ${2|GET,POST,PUT,PATCH,DELETE|} ${3:/api/users} {",
|
||||||
|
" request {",
|
||||||
|
" body {",
|
||||||
|
" ${4:name}?: ${5:string}",
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
"",
|
||||||
|
" response {",
|
||||||
|
" return ${6:data}",
|
||||||
|
" }",
|
||||||
|
"",
|
||||||
|
" error {",
|
||||||
|
" return ${7:null}",
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "apis entry",
|
||||||
|
detail: "Single API call declaration",
|
||||||
|
documentation:
|
||||||
|
"Declare a single named API call inside an apis { } block: <name> <METHOD> <path> { request/response/error }.",
|
||||||
|
snippet: [
|
||||||
|
"${1:searchUsers} ${2|GET,POST,PUT,PATCH,DELETE|} ${3:/api/users} {",
|
||||||
|
" request {",
|
||||||
|
" body {",
|
||||||
|
" ${4:name}?: ${5:string}",
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
"",
|
||||||
|
" response {",
|
||||||
|
" return ${6:data}",
|
||||||
|
" }",
|
||||||
|
"",
|
||||||
|
" error {",
|
||||||
|
" return ${7:null}",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "persist",
|
label: "persist",
|
||||||
detail: "Include-only store persistence",
|
detail: "Include-only store persistence",
|
||||||
@@ -570,6 +618,41 @@ function isInsideWatch(document, position) {
|
|||||||
return depth > 0;
|
return depth > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether an offset sits inside a `view { }` block.
|
||||||
|
*
|
||||||
|
* The language server owns completion there and returns a merged list, so this
|
||||||
|
* provider stands down to avoid VS Code concatenating two independent lists.
|
||||||
|
* Quotes are only tracked inside a tag: `<p>it's</p>` would otherwise open a
|
||||||
|
* string that never closes.
|
||||||
|
*/
|
||||||
|
function isInsideViewBlock(text, offset) {
|
||||||
|
const pattern = /\bview\s*\{/g;
|
||||||
|
let match;
|
||||||
|
while ((match = pattern.exec(text))) {
|
||||||
|
const start = match.index + match[0].length;
|
||||||
|
let depth = 1;
|
||||||
|
let inTag = false;
|
||||||
|
let quote = null;
|
||||||
|
let index = start;
|
||||||
|
for (; index < text.length && depth > 0; index += 1) {
|
||||||
|
const char = text[index];
|
||||||
|
if (quote) {
|
||||||
|
if (char === quote) quote = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inTag && (char === '"' || char === "'")) quote = char;
|
||||||
|
else if (char === "<") inTag = true;
|
||||||
|
else if (char === ">") inTag = false;
|
||||||
|
else if (char === "{") depth += 1;
|
||||||
|
else if (char === "}") depth -= 1;
|
||||||
|
}
|
||||||
|
if (offset >= start && offset <= index) return true;
|
||||||
|
pattern.lastIndex = index;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function isAfterWatchKeyword(document, position) {
|
function isAfterWatchKeyword(document, position) {
|
||||||
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
||||||
|
|
||||||
@@ -634,6 +717,8 @@ function addFunctionCompletions(items, document) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function provideCompletionItems(document, position) {
|
function provideCompletionItems(document, position) {
|
||||||
|
if (isInsideViewBlock(document.getText(), document.offsetAt(position))) return [];
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
|
||||||
@@ -707,6 +792,7 @@ module.exports = {
|
|||||||
extractProps,
|
extractProps,
|
||||||
extractRouteParams,
|
extractRouteParams,
|
||||||
extractStates,
|
extractStates,
|
||||||
|
isInsideViewBlock,
|
||||||
provideCompletionItems,
|
provideCompletionItems,
|
||||||
registerCompletionProvider,
|
registerCompletionProvider,
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
|||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const vscode = require("vscode");
|
const vscode = require("vscode");
|
||||||
const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
|
const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
|
||||||
|
const { registerAutoCloseTags } = require("./auto-close-tags.js");
|
||||||
|
|
||||||
const WRN_LANGUAGE_ID = "wrn";
|
const WRN_LANGUAGE_ID = "wrn";
|
||||||
/** @type {LanguageClient | undefined} */
|
/** @type {LanguageClient | undefined} */
|
||||||
@@ -43,6 +44,7 @@ async function activate(context) {
|
|||||||
{ documentSelector: [{ scheme: "file", language: WRN_LANGUAGE_ID }] },
|
{ documentSelector: [{ scheme: "file", language: WRN_LANGUAGE_ID }] },
|
||||||
);
|
);
|
||||||
await client.start();
|
await client.start();
|
||||||
|
registerAutoCloseTags(context, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deactivate() {
|
async function deactivate() {
|
||||||
@@ -51,4 +53,4 @@ async function deactivate() {
|
|||||||
if (running) await running.stop();
|
if (running) await running.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { activate, deactivate, recoverWrnLanguage };
|
module.exports = { activate, deactivate, recoverWrnLanguage, registerAutoCloseTags };
|
||||||
|
|||||||
+23128
-164
File diff suppressed because one or more lines are too long
@@ -113,10 +113,10 @@
|
|||||||
"include": "#action-block"
|
"include": "#action-block"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "#mode-block"
|
"include": "#api-block"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "#api-block"
|
"include": "#apis-block"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "#functions-block"
|
"include": "#functions-block"
|
||||||
@@ -513,8 +513,8 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mode-block": {
|
"apis-block": {
|
||||||
"begin": "\\b(ssr|client)\\b\\s*(\\{)",
|
"begin": "\\b(apis)\\b\\s*(\\{)",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"1": {
|
"1": {
|
||||||
"name": "keyword.control.wrn"
|
"name": "keyword.control.wrn"
|
||||||
@@ -534,21 +534,38 @@
|
|||||||
"include": "#comments"
|
"include": "#comments"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"begin": "\\b(api)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)",
|
"begin": "\\b([A-Za-z_$][A-Za-z0-9_$]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)",
|
||||||
|
"beginCaptures": {
|
||||||
|
"1": {
|
||||||
|
"name": "entity.name.function.wrn"
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
"name": "constant.language.http-method.wrn"
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
"name": "string.unquoted.route.wrn"
|
||||||
|
},
|
||||||
|
"4": {
|
||||||
|
"name": "punctuation.definition.block.begin.wrn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"end": "\\}",
|
||||||
|
"endCaptures": {
|
||||||
|
"0": {
|
||||||
|
"name": "punctuation.definition.block.end.wrn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"include": "#comments"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"begin": "\\b(request|response|error)\\b\\s*(\\{)",
|
||||||
"beginCaptures": {
|
"beginCaptures": {
|
||||||
"1": {
|
"1": {
|
||||||
"name": "keyword.control.wrn"
|
"name": "keyword.control.wrn"
|
||||||
},
|
},
|
||||||
"2": {
|
"2": {
|
||||||
"name": "entity.name.function.wrn"
|
|
||||||
},
|
|
||||||
"3": {
|
|
||||||
"name": "constant.language.http-method.wrn"
|
|
||||||
},
|
|
||||||
"4": {
|
|
||||||
"name": "string.unquoted.route.wrn"
|
|
||||||
},
|
|
||||||
"5": {
|
|
||||||
"name": "punctuation.definition.block.begin.wrn"
|
"name": "punctuation.definition.block.begin.wrn"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -569,7 +586,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"include": "#functions-block"
|
"include": "#ts-braces"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"include": "source.ts"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const { installVsCodeHost } = require("./vscode-host.js");
|
||||||
|
|
||||||
|
class Position {
|
||||||
|
constructor(line, character) {
|
||||||
|
this.line = line;
|
||||||
|
this.character = character;
|
||||||
|
}
|
||||||
|
translate(lineDelta, characterDelta) {
|
||||||
|
return new Position(this.line + lineDelta, this.character + characterDelta);
|
||||||
|
}
|
||||||
|
isEqual(other) {
|
||||||
|
return this.line === other.line && this.character === other.character;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Selection {
|
||||||
|
constructor(active) {
|
||||||
|
this.active = active;
|
||||||
|
this.anchor = active;
|
||||||
|
this.isEmpty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SnippetString {
|
||||||
|
constructor(value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let changeListener = null;
|
||||||
|
const host = {
|
||||||
|
Position,
|
||||||
|
Selection,
|
||||||
|
SnippetString,
|
||||||
|
workspace: {
|
||||||
|
onDidChangeTextDocument(listener) {
|
||||||
|
changeListener = listener;
|
||||||
|
return { dispose() {} };
|
||||||
|
},
|
||||||
|
getConfiguration() {
|
||||||
|
return { get: (_key, fallback) => fallback };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
window: { activeTextEditor: null },
|
||||||
|
};
|
||||||
|
|
||||||
|
const restoreHost = installVsCodeHost(host);
|
||||||
|
const { registerAutoCloseTags } = require("../src/auto-close-tags.js");
|
||||||
|
restoreHost();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drive the handler the way VS Code does: the document has already been
|
||||||
|
* updated and the carets moved by the time the change event fires.
|
||||||
|
*/
|
||||||
|
function scenario({ carets, snippetFor, typed = ">" }) {
|
||||||
|
const inserted = [];
|
||||||
|
const asked = [];
|
||||||
|
const document = { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } };
|
||||||
|
const editor = {
|
||||||
|
document,
|
||||||
|
selections: carets.map((caret) => new Selection(caret)),
|
||||||
|
insertSnippet(snippet, positions) {
|
||||||
|
inserted.push({ value: snippet.value, positions });
|
||||||
|
return Promise.resolve(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
editor.selection = editor.selections[0];
|
||||||
|
host.window.activeTextEditor = editor;
|
||||||
|
|
||||||
|
const client = {
|
||||||
|
sendRequest(_method, params) {
|
||||||
|
asked.push(params.position);
|
||||||
|
return Promise.resolve(snippetFor(params.position));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
registerAutoCloseTags({ subscriptions: [] }, client);
|
||||||
|
|
||||||
|
return {
|
||||||
|
inserted,
|
||||||
|
asked,
|
||||||
|
fire: () =>
|
||||||
|
changeListener({
|
||||||
|
document,
|
||||||
|
// Pre-edit coordinates, deliberately not usable as caret positions.
|
||||||
|
contentChanges: carets.map(() => ({
|
||||||
|
text: typed,
|
||||||
|
rangeLength: 0,
|
||||||
|
range: { start: new Position(0, 0) },
|
||||||
|
})),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("closes the tag at a single caret", async () => {
|
||||||
|
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
|
||||||
|
await run.fire();
|
||||||
|
|
||||||
|
assert.equal(run.inserted.length, 1);
|
||||||
|
assert.equal(run.inserted[0].value, "$0</div>");
|
||||||
|
assert.deepEqual(
|
||||||
|
run.inserted[0].positions.map((p) => [p.line, p.character]),
|
||||||
|
[[1, 8]],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("closes the tag at every caret in one insertion", async () => {
|
||||||
|
// One insertSnippet call is what keeps all the carets alive: inserting
|
||||||
|
// sequentially would collapse the selection to the first snippet.
|
||||||
|
const run = scenario({
|
||||||
|
carets: [new Position(1, 8), new Position(2, 8), new Position(3, 8)],
|
||||||
|
snippetFor: () => "$0</div>",
|
||||||
|
});
|
||||||
|
await run.fire();
|
||||||
|
|
||||||
|
assert.equal(run.asked.length, 3);
|
||||||
|
assert.equal(run.inserted.length, 1);
|
||||||
|
assert.deepEqual(
|
||||||
|
run.inserted[0].positions.map((p) => [p.line, p.character]),
|
||||||
|
[
|
||||||
|
[1, 8],
|
||||||
|
[2, 8],
|
||||||
|
[3, 8],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("asks about each caret's own position rather than the change ranges", async () => {
|
||||||
|
// Every contentChange above reports (0, 0). Using those would query and
|
||||||
|
// insert at the wrong offsets once more than one caret is on a line.
|
||||||
|
const run = scenario({
|
||||||
|
carets: [new Position(4, 12), new Position(9, 3)],
|
||||||
|
snippetFor: () => "$0</p>",
|
||||||
|
});
|
||||||
|
await run.fire();
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
run.asked.map((p) => [p.line, p.character]),
|
||||||
|
[
|
||||||
|
[4, 12],
|
||||||
|
[9, 3],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("declines when the carets want different closing tags", async () => {
|
||||||
|
const run = scenario({
|
||||||
|
carets: [new Position(1, 8), new Position(2, 8)],
|
||||||
|
snippetFor: (position) => (position.line === 1 ? "$0</div>" : "$0</span>"),
|
||||||
|
});
|
||||||
|
await run.fire();
|
||||||
|
|
||||||
|
assert.equal(run.inserted.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("declines when any caret has no tag to close", async () => {
|
||||||
|
const run = scenario({
|
||||||
|
carets: [new Position(1, 8), new Position(2, 8)],
|
||||||
|
snippetFor: (position) => (position.line === 1 ? "$0</br>" : null),
|
||||||
|
});
|
||||||
|
await run.fire();
|
||||||
|
|
||||||
|
assert.equal(run.inserted.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("declines a replaced selection", async () => {
|
||||||
|
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
|
||||||
|
await changeListener({
|
||||||
|
document: { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } },
|
||||||
|
contentChanges: [{ text: ">", rangeLength: 3, range: { start: new Position(1, 5) } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(run.inserted.length, 0);
|
||||||
|
});
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { installVsCodeHost } = require("./vscode-host.js");
|
||||||
|
|
||||||
|
const restoreHost = installVsCodeHost({
|
||||||
|
Position: class Position {
|
||||||
|
constructor(line, character) {
|
||||||
|
this.line = line;
|
||||||
|
this.character = character;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Range: class Range {
|
||||||
|
constructor(start, end) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CompletionItem: class CompletionItem {
|
||||||
|
constructor(label, kind) {
|
||||||
|
this.label = label;
|
||||||
|
this.kind = kind;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CompletionItemKind: {
|
||||||
|
Event: 23,
|
||||||
|
Property: 10,
|
||||||
|
Function: 12,
|
||||||
|
Keyword: 14,
|
||||||
|
Variable: 13,
|
||||||
|
},
|
||||||
|
SnippetString: class SnippetString {
|
||||||
|
constructor(text) {
|
||||||
|
this.value = text;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MarkdownString: class MarkdownString {
|
||||||
|
constructor(text) {
|
||||||
|
this.value = text;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { isInsideViewBlock, provideCompletionItems } = require("../src/completion.js");
|
||||||
|
restoreHost();
|
||||||
|
|
||||||
|
const PAGE = `page Home {
|
||||||
|
view {
|
||||||
|
<div>hello</div>
|
||||||
|
}
|
||||||
|
functions {
|
||||||
|
function go() {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("a markup offset is inside a view block", () => {
|
||||||
|
assert.equal(isInsideViewBlock(PAGE, PAGE.indexOf("<div")), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a functions-block offset is not inside a view block", () => {
|
||||||
|
assert.equal(isInsideViewBlock(PAGE, PAGE.indexOf("function go")), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("provideCompletionItems returns empty array when inside view block", () => {
|
||||||
|
const document = {
|
||||||
|
getText() {
|
||||||
|
return PAGE;
|
||||||
|
},
|
||||||
|
offsetAt() {
|
||||||
|
return PAGE.indexOf("<div");
|
||||||
|
},
|
||||||
|
lineAt() {
|
||||||
|
return { text: "<div>hello</div>" };
|
||||||
|
},
|
||||||
|
fileName: "test.wrn",
|
||||||
|
};
|
||||||
|
const position = { line: 2, character: 4 };
|
||||||
|
|
||||||
|
const result = provideCompletionItems(document, position);
|
||||||
|
assert.equal(Array.isArray(result), true);
|
||||||
|
assert.equal(result.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("provideCompletionItems returns non-empty array when inside functions block", () => {
|
||||||
|
const document = {
|
||||||
|
getText() {
|
||||||
|
return PAGE;
|
||||||
|
},
|
||||||
|
offsetAt() {
|
||||||
|
return PAGE.indexOf("function go");
|
||||||
|
},
|
||||||
|
lineAt() {
|
||||||
|
return { text: " function go() {}" };
|
||||||
|
},
|
||||||
|
fileName: "test.wrn",
|
||||||
|
};
|
||||||
|
const position = { line: 5, character: 4 };
|
||||||
|
|
||||||
|
const result = provideCompletionItems(document, position);
|
||||||
|
assert.equal(Array.isArray(result), true);
|
||||||
|
assert(result.length > 0, "should return non-empty completions outside view block");
|
||||||
|
});
|
||||||
@@ -2,17 +2,13 @@
|
|||||||
|
|
||||||
const assert = require("node:assert");
|
const assert = require("node:assert");
|
||||||
const { test } = require("node:test");
|
const { test } = require("node:test");
|
||||||
const Module = require("node:module");
|
const { installVsCodeHost } = require("./vscode-host.js");
|
||||||
|
|
||||||
// These extraction helpers are pure, but their module also registers VS Code
|
// These extraction helpers are pure, but their module also registers VS Code
|
||||||
// providers at runtime. Supply a minimal host shim for unit tests.
|
// providers at runtime. Supply a minimal host shim for unit tests.
|
||||||
const originalLoad = Module._load;
|
const restoreHost = installVsCodeHost({});
|
||||||
Module._load = function load(request, parent, isMain) {
|
|
||||||
if (request === "vscode") return {};
|
|
||||||
return originalLoad.call(this, request, parent, isMain);
|
|
||||||
};
|
|
||||||
const { extractRouteParams, extractStates } = require("../src/completion");
|
const { extractRouteParams, extractStates } = require("../src/completion");
|
||||||
Module._load = originalLoad;
|
restoreHost();
|
||||||
|
|
||||||
test("extracts dynamic route params from filename", () => {
|
test("extracts dynamic route params from filename", () => {
|
||||||
const document = {
|
const document = {
|
||||||
|
|||||||
@@ -2,12 +2,9 @@
|
|||||||
|
|
||||||
const assert = require("node:assert");
|
const assert = require("node:assert");
|
||||||
const { test } = require("node:test");
|
const { test } = require("node:test");
|
||||||
const Module = require("node:module");
|
const { installVsCodeHost } = require("./vscode-host.js");
|
||||||
|
|
||||||
const originalLoad = Module._load;
|
const restoreHost = installVsCodeHost({
|
||||||
Module._load = function load(request, parent, isMain) {
|
|
||||||
if (request === "vscode") {
|
|
||||||
return {
|
|
||||||
Diagnostic: class Diagnostic {
|
Diagnostic: class Diagnostic {
|
||||||
constructor(range, message, severity) {
|
constructor(range, message, severity) {
|
||||||
this.range = range;
|
this.range = range;
|
||||||
@@ -22,10 +19,7 @@ Module._load = function load(request, parent, isMain) {
|
|||||||
this.end = end;
|
this.end = end;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
|
||||||
return originalLoad.call(this, request, parent, isMain);
|
|
||||||
};
|
|
||||||
const {
|
const {
|
||||||
findTopLevelDeclaration,
|
findTopLevelDeclaration,
|
||||||
maskLeadingTrivia,
|
maskLeadingTrivia,
|
||||||
@@ -34,7 +28,7 @@ const {
|
|||||||
validateLayoutUsage,
|
validateLayoutUsage,
|
||||||
validateRootMembers,
|
validateRootMembers,
|
||||||
} = require("../src/diagnostics");
|
} = require("../src/diagnostics");
|
||||||
Module._load = originalLoad;
|
restoreHost();
|
||||||
|
|
||||||
function mockDocument() {
|
function mockDocument() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ test("preserves nested prop defaults while formatting", () => {
|
|||||||
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
|
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("formats large typed JSON prop defaults with the bundled formatter", () => {
|
||||||
|
const source = `component Catalog {
|
||||||
|
props {
|
||||||
|
categories: unknown[] = [{"label":"Platform & Core","apps":[{"label":"Home","href":"/home"},{"label":"SSO","href":"/sso"}]},{"label":"Sales","apps":[{"label":"CRM","href":"/crm"}]}]
|
||||||
|
}
|
||||||
|
view { <div></div> }
|
||||||
|
}`;
|
||||||
|
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2, printWidth: 70 });
|
||||||
|
|
||||||
|
assert.match(formatted, /categories: unknown\[\] = \[\n \{/);
|
||||||
|
assert.match(formatted, /"apps": \[\n \{/);
|
||||||
|
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2, printWidth: 70 }), formatted);
|
||||||
|
});
|
||||||
|
|
||||||
test("formats native JSON state values with readable indentation", () => {
|
test("formats native JSON state values with readable indentation", () => {
|
||||||
const source = `component Footer {
|
const source = `component Footer {
|
||||||
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
|
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const { readFileSync } = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const grammarPath = join(__dirname, "../syntaxes/wrn.tmLanguage.json");
|
||||||
|
const grammar = JSON.parse(readFileSync(grammarPath, "utf8"));
|
||||||
|
|
||||||
|
test("the grammar knows the apis block", () => {
|
||||||
|
assert.ok(grammar.repository["apis-block"], "apis should appear as a block keyword");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("client keeps its highlighting where it is still valid", () => {
|
||||||
|
// client state {}, runtime = "client", and client function all survive.
|
||||||
|
// Only the client {} data block was removed.
|
||||||
|
assert.match(JSON.stringify(grammar), /client/, "client must still be matched");
|
||||||
|
assert.match(
|
||||||
|
JSON.stringify(grammar),
|
||||||
|
/shared/,
|
||||||
|
"the shared function modifier must still be matched",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Structural assertions: these fail if the rule they name is deleted, ---
|
||||||
|
// --- even though the substrings "apis"/"client" remain elsewhere in the ---
|
||||||
|
// --- file (e.g. inside unrelated pattern names or comments). ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* vscode-textmate is not a dependency of this repo (confirmed via
|
||||||
|
* `require.resolve`), so real tokenization is unavailable here. Instead we
|
||||||
|
* pull each rule's own `begin`/`match` regex out of the parsed grammar
|
||||||
|
* object and exercise it directly with the JS regex engine. The grammar's
|
||||||
|
* patterns use only `\b`, character classes, and `(?<=...)` lookbehind --
|
||||||
|
* all supported by native JS regexes -- so this is a faithful proxy for
|
||||||
|
* "does this specific rule match this specific source line" without
|
||||||
|
* needing an Oniguruma-backed tokenizer.
|
||||||
|
*/
|
||||||
|
function ruleByTopLevelBlockName(name) {
|
||||||
|
const entry = grammar.repository[name];
|
||||||
|
assert.ok(entry, `repository.${name} should exist`);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function topLevelBlockIncludes(name) {
|
||||||
|
return grammar.repository.blocks.patterns.some((p) => p.include === `#${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("blocks includes the apis-block pattern", () => {
|
||||||
|
assert.ok(topLevelBlockIncludes("apis-block"), "#blocks must include #apis-block");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the apis-block begin pattern matches the container keyword", () => {
|
||||||
|
const rule = ruleByTopLevelBlockName("apis-block");
|
||||||
|
const beginRe = new RegExp(rule.begin);
|
||||||
|
assert.ok(beginRe.test("apis {"), "apis-block begin should match 'apis {'");
|
||||||
|
assert.ok(!beginRe.test("ssr {"), "apis-block begin must not match 'ssr {'");
|
||||||
|
assert.ok(!beginRe.test("client {"), "apis-block begin must not match 'client {'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the apis-block declares an entry pattern for <name> <METHOD> <path>", () => {
|
||||||
|
const rule = ruleByTopLevelBlockName("apis-block");
|
||||||
|
const entryPattern = (rule.patterns || []).find(
|
||||||
|
(p) => p.name === "apis-entry" || (p.begin && /GET\|POST/.test(p.begin)),
|
||||||
|
);
|
||||||
|
assert.ok(entryPattern, "apis-block should contain an entry declaration pattern");
|
||||||
|
const entryRe = new RegExp(entryPattern.begin || entryPattern.match);
|
||||||
|
assert.ok(
|
||||||
|
entryRe.test("searchUsers POST /api/users {"),
|
||||||
|
"the entry pattern should match '<name> <METHOD> <path> {'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the ssr/client data-block pattern (mode-block) is gone", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
grammar.repository["mode-block"],
|
||||||
|
undefined,
|
||||||
|
"mode-block (the removed ssr {}/client {} data-block rule) must be deleted from the repository",
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
!topLevelBlockIncludes("mode-block"),
|
||||||
|
"#blocks must no longer include the removed #mode-block pattern",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Regression coverage: constructs that must NOT be broken by this change ---
|
||||||
|
|
||||||
|
test("REGRESSION: client state {} still highlights via grouped-state-block", () => {
|
||||||
|
const rule = ruleByTopLevelBlockName("grouped-state-block");
|
||||||
|
const beginRe = new RegExp(rule.begin);
|
||||||
|
assert.ok(
|
||||||
|
beginRe.test("client state {"),
|
||||||
|
"grouped-state-block must still match 'client state {'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('REGRESSION: runtime = "client" still highlights via execution-decl', () => {
|
||||||
|
const rule = ruleByTopLevelBlockName("execution-decl");
|
||||||
|
const beginRe = new RegExp(rule.begin);
|
||||||
|
assert.ok(
|
||||||
|
beginRe.test('runtime = "client"'),
|
||||||
|
"execution-decl must still match 'runtime = \"client\"'",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("REGRESSION: the client function modifier still highlights via functions-block", () => {
|
||||||
|
const rule = ruleByTopLevelBlockName("functions-block");
|
||||||
|
const matchRe = new RegExp(
|
||||||
|
rule.patterns.find(
|
||||||
|
(p) => p.captures && p.captures["1"]?.name === "storage.modifier.runtime.wrn",
|
||||||
|
).match,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
matchRe.test("client async function go(): Promise<void> {"),
|
||||||
|
"functions-block must still recognize the 'client' runtime modifier on a function",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("REGRESSION: the client function modifier also highlights via v060-keywords", () => {
|
||||||
|
const rule = ruleByTopLevelBlockName("v060-keywords");
|
||||||
|
const modifierPattern = rule.patterns.find((p) => p.name === "storage.modifier.runtime.wrn");
|
||||||
|
assert.ok(modifierPattern, "v060-keywords should have a storage.modifier.runtime.wrn rule");
|
||||||
|
const matchRe = new RegExp(modifierPattern.match);
|
||||||
|
assert.ok(
|
||||||
|
matchRe.test("client function go"),
|
||||||
|
"v060-keywords must still recognize 'client' before 'function'",
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
matchRe.test("client state"),
|
||||||
|
"v060-keywords must still recognize 'client' before 'state'",
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -113,6 +113,21 @@ try {
|
|||||||
readFileSync(join(root, rel), "utf8");
|
readFileSync(join(root, rel), "utf8");
|
||||||
ok(`Marketplace document exists: ${rel}`);
|
ok(`Marketplace document exists: ${rel}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emmetLanguages = manifest.contributes?.configurationDefaults?.["emmet.includeLanguages"];
|
||||||
|
emmetLanguages?.wrn === "html"
|
||||||
|
? ok("Emmet is mapped for wrn documents")
|
||||||
|
: bad("Emmet is mapped for wrn documents", `got ${JSON.stringify(emmetLanguages)}`);
|
||||||
|
|
||||||
|
const autoClose =
|
||||||
|
manifest.contributes?.configuration?.properties?.["wrnexus.html.autoClosingTags"];
|
||||||
|
autoClose?.type === "boolean" && autoClose?.default === true
|
||||||
|
? ok("auto-closing tags setting is contributed")
|
||||||
|
: bad("auto-closing tags setting is contributed", `got ${JSON.stringify(autoClose)}`);
|
||||||
|
|
||||||
|
manifest.dependencies?.["vscode-html-languageservice"]
|
||||||
|
? ok("HTML language service ships as a runtime dependency")
|
||||||
|
: bad("HTML language service ships as a runtime dependency");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
bad("Marketplace metadata", e.message);
|
bad("Marketplace metadata", e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supply a stub `vscode` host so extension sources can be unit tested.
|
||||||
|
*
|
||||||
|
* These files run under `node --test` (see the package's test script), where
|
||||||
|
* patching `Module._load` is enough. A bare `bun test` from the repository
|
||||||
|
* root also picks them up by filename, and Bun resolves `require` through its
|
||||||
|
* own resolver without consulting `Module._load` -- so under Bun the same
|
||||||
|
* files failed with "Cannot find package 'vscode'". Registering a virtual
|
||||||
|
* module covers that case, leaving one shim that works under both runners.
|
||||||
|
*
|
||||||
|
* Returns a function restoring the original loader.
|
||||||
|
*/
|
||||||
|
function installVsCodeHost(stub) {
|
||||||
|
const Module = require("node:module");
|
||||||
|
|
||||||
|
if (typeof Bun !== "undefined") {
|
||||||
|
require("bun").plugin({
|
||||||
|
name: "vscode-host-stub",
|
||||||
|
setup(build) {
|
||||||
|
build.module("vscode", () => ({ exports: stub, loader: "object" }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalLoad = Module._load;
|
||||||
|
Module._load = function load(request, parent, isMain) {
|
||||||
|
if (request === "vscode") return stub;
|
||||||
|
return originalLoad.call(this, request, parent, isMain);
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Module._load = originalLoad;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { installVsCodeHost };
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
# WrNexus app - instructions for AI coding assistants
|
||||||
|
|
||||||
|
This is a **WrNexus** app. When creating or editing pages, components, API routes,
|
||||||
|
or features, follow the framework conventions below. WrNexus is private and not in
|
||||||
|
your training data, so rely on these rules - do NOT assume React/Next.js/Vue patterns.
|
||||||
|
|
||||||
|
# WrNexus
|
||||||
|
|
||||||
|
> WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in
|
||||||
|
> `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based.
|
||||||
|
> This document teaches an AI how to write correct WrNexus code. It is private and
|
||||||
|
> post-dates model training data, so rely on THIS document, not prior web-framework
|
||||||
|
> assumptions.
|
||||||
|
|
||||||
|
## Golden rules
|
||||||
|
|
||||||
|
- **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React
|
||||||
|
for UI. Do NOT use `useState`, hooks, JSX, or a client bundler.
|
||||||
|
- **Routing is file-based** under `app/`. The filename is the route. No router config.
|
||||||
|
- **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render
|
||||||
|
on the server and hydrate automatically — you never write client-side JS islands.
|
||||||
|
- **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported.
|
||||||
|
- To add files, prefer the CLI: `wrnexus generate page <Name>` / `component <name>` / `api <path>` / `schema <name>`.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
pages/ *.wrn → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug"
|
||||||
|
components/ *.wrn → reusable UI, mounted in a page/component via <div data-component="name" ...props>
|
||||||
|
layouts/ *.wrn → named layouts; a page opts in with layout = "name"
|
||||||
|
api/ *.ts → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response
|
||||||
|
middleware/ *.ts → export default async (ctx, next) => next()
|
||||||
|
realtime/ *.ts → export default defineRoom({ ... }) from "@wrnexus/core" (ws://host/realtime/<name>)
|
||||||
|
schemas/ *.ts → validation schemas (the `v` builder), used by forms + parseBody
|
||||||
|
locales/ *.json → i18n messages per language
|
||||||
|
db/ schema.ts, queries/*.sql, migrations/*.sql
|
||||||
|
styles/ global.css → Tailwind (default) or plain CSS
|
||||||
|
wrnexus.config.ts → app config (AppConfig from "@wrnexus/styles")
|
||||||
|
public/ → static assets served at /
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.wrn` page
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
page Home {
|
||||||
|
layout = "public" // optional: a component in app/layouts/<name>.wrn ("none" to skip)
|
||||||
|
|
||||||
|
state count = 0 // optional: seeds client-reactive state (omit for pure SSR)
|
||||||
|
|
||||||
|
seo {
|
||||||
|
title = "Home"
|
||||||
|
description = "..."
|
||||||
|
canonical = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<h1>Hello</h1>
|
||||||
|
<p>Count is {count}, doubled is {count * 2}.</p>
|
||||||
|
<button @click="count++">Increment</button>
|
||||||
|
<div data-component="counter" start="5" label="Clicks"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
style {
|
||||||
|
h1 { color: var(--wrn-color-text); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.wrn` component
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
component Counter {
|
||||||
|
props { // props come from mount attributes; each is coerced to the
|
||||||
|
start = 0 // TYPE of its default (so start="5" arrives as the number 5)
|
||||||
|
label = "Count"
|
||||||
|
}
|
||||||
|
state count = start // state may reference props
|
||||||
|
view {
|
||||||
|
<button @click="count++">{label}: {count}</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Mount it from any page/component: `<div data-component="counter" start="0" label="Clicks"></div>`.
|
||||||
|
Components render on the server with their props, then hydrate — no per-component JS.
|
||||||
|
|
||||||
|
## The `view { }` block (plain HTML + a few directives)
|
||||||
|
|
||||||
|
- `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`.
|
||||||
|
- `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`.
|
||||||
|
- `<div data-component="name" prop="v">` — mount a component (attrs become string props, coerced).
|
||||||
|
- `<slot></slot>` / `<slot name="x"></slot>` — component/layout slots; fill with `<div data-slot="x">…</div>`.
|
||||||
|
- **Server loop (DB/list/table):** `{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `<list>` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`.
|
||||||
|
- **Server conditional:** `{#if <expr>} … {:else if <expr>} … {:else} … {/if}` — renders the first truthy branch on the server. `<expr>` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}<span>●</span>{:else}<span>○</span>{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead.
|
||||||
|
- i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`.
|
||||||
|
- Theme: any element with `data-wrn-theme-toggle` toggles light/dark; `data-wrn-theme-set="dark"` sets it.
|
||||||
|
- Void/self-closing tags are fine: `<br />`, `<img src="..." />`.
|
||||||
|
- Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text.
|
||||||
|
|
||||||
|
## Data-driven tables / lists (server-rendered `.wrn`)
|
||||||
|
|
||||||
|
Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them.
|
||||||
|
This renders on the **server** (SSR-first) and is HTML-escaped by default.
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
page Admin {
|
||||||
|
layout = "dashboard"
|
||||||
|
|
||||||
|
// Fetch on the server. The api handler at /api/contacts returns { contacts: [...] };
|
||||||
|
// this block's `return contacts` exposes that array (via `$data`) as the binding `rows`.
|
||||||
|
ssr {
|
||||||
|
api rows GET /api/contacts { return contacts }
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{#each rows as r, i}
|
||||||
|
<tr>
|
||||||
|
<td>#{i}</td>
|
||||||
|
<td>{r.name}</td>
|
||||||
|
<td><a href="mailto:{r.email}">{r.email}</a></td>
|
||||||
|
</tr>
|
||||||
|
{:empty}
|
||||||
|
<tr><td colspan="3">No submissions yet.</td></tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The matching API returns the array under a key the `ssr` block reads:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/api/contacts.ts → GET /api/contacts
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
export const GET = async () => {
|
||||||
|
const contacts = await getDb().all("SELECT id, name, email FROM contacts ORDER BY id DESC");
|
||||||
|
return Response.json({ contacts }); // ssr block does `return contacts`
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prefer this `.wrn` + `{#each}` approach for DB-backed tables and lists.** (`.ts`/`.tsx`
|
||||||
|
pages returning an HTML string are also supported for fully-custom programmatic rendering,
|
||||||
|
but a `.wrn` page with `ssr` data + `{#each}` is the idiomatic, SSR-first way.)
|
||||||
|
|
||||||
|
## API routes (`app/api/*.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/api/users/list.ts → GET /api/users/list
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
|
||||||
|
export const GET = async (ctx) => {
|
||||||
|
return Response.json({ users: await ListUsers(getDb()) });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = async (ctx) => {
|
||||||
|
const body = await ctx.req.json();
|
||||||
|
return Response.json({ ok: true, body }, { status: 201 });
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`ctx` (the `Context` from `@wrnexus/core`) has:
|
||||||
|
`req: Request`, `url: URL`, `params: Record<string,string>` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`),
|
||||||
|
`lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`.
|
||||||
|
|
||||||
|
When an SSO forward-auth verifier needs the URL that originally reached the gateway, use
|
||||||
|
`@wrnexus/helpers` instead of constructing it from untrusted headers:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { redirectToLogin } from "@wrnexus/helpers";
|
||||||
|
|
||||||
|
return redirectToLogin(ctx, "/login", {
|
||||||
|
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The package also exports `getOriginalRequestUrl`, `getOriginalRequestOrigin`,
|
||||||
|
`getOriginalRequestPath`, and `getOriginalRequestMethod`. Always pass `allowedHosts` when
|
||||||
|
using forwarded gateway URLs; the helper rejects untrusted redirect destinations.
|
||||||
|
|
||||||
|
## Middleware & realtime
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/middleware/logger.ts
|
||||||
|
export default async function logger(ctx, next) {
|
||||||
|
console.log(ctx.req.method, ctx.url.pathname);
|
||||||
|
return next(); // return a Response WITHOUT calling next() to short-circuit
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/realtime/chat.ts → ws://host/realtime/chat
|
||||||
|
import { defineRoom } from "@wrnexus/core";
|
||||||
|
export default defineRoom({
|
||||||
|
onConnect(client) {
|
||||||
|
client.send({ type: "system", text: "connected" });
|
||||||
|
},
|
||||||
|
onMessage(client, msg) {
|
||||||
|
client.room.broadcast({ type: "message", data: msg });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Client side: a page opts in with `data-room="chat"` (handled by the realtime runtime).
|
||||||
|
|
||||||
|
## Config (`wrnexus.config.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { AppConfig } from "@wrnexus/styles";
|
||||||
|
const config: AppConfig = {
|
||||||
|
seo: { title: "App", titleTemplate: "%s | App", description: "..." },
|
||||||
|
styles: {
|
||||||
|
entry: "app/styles/global.css",
|
||||||
|
process: async ({ entryPath, mode }) => /* Tailwind */ "",
|
||||||
|
},
|
||||||
|
fonts: {
|
||||||
|
sans: '"Inter", system-ui, sans-serif',
|
||||||
|
google: [{ family: "Inter", weights: [400, 600] }],
|
||||||
|
},
|
||||||
|
theme: { default: "dark", themes: { light: { "color-primary": "#2563eb" } } },
|
||||||
|
i18n: { default: "en", locales: ["en", "es"] },
|
||||||
|
db: { driver: "sqlite", url: "file:./dev.db" },
|
||||||
|
security: { cors: { enabled: true, origin: ["http://localhost:5173"] } },
|
||||||
|
// profiles: { production: { db: { driver: "postgres", url: process.env.DATABASE_URL } } },
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database (`@wrnexus/db`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/db/schema.ts
|
||||||
|
import { v, table } from "@wrnexus/db";
|
||||||
|
export const users = table("users", {
|
||||||
|
id: v.id(),
|
||||||
|
name: v.string(),
|
||||||
|
email: v.string().unique(),
|
||||||
|
createdAt: v.timestamp(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- Queries: write `app/db/queries/*.sql` with `-- name: ListUsers :many` blocks; `wrnexus db generate` emits typed functions.
|
||||||
|
- Access at runtime: `import { getDb } from "@wrnexus/db"; const rows = await ListUsers(getDb());`
|
||||||
|
- Migrations in `app/db/migrations/`; run `wrnexus db migrate` (dev auto-migrates sqlite).
|
||||||
|
|
||||||
|
## Validation (`@wrnexus/validation`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/schemas/login.ts
|
||||||
|
import { v } from "@wrnexus/validation";
|
||||||
|
export default v.object({
|
||||||
|
email: v.string().email(),
|
||||||
|
password: v.string().min(8),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In an API route: `import s from "../schemas/login"; import { parseBody } from "@wrnexus/validation"; const r = await parseBody(s, ctx.req);` → `r.ok ? r.value : r.response`.
|
||||||
|
In a form: `<form data-schema="login" action="/api/login" method="post">` + `<span data-error="email"></span>` (client + server validation connected automatically).
|
||||||
|
|
||||||
|
## AI / LLM (`@wrnexus/ai`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/api/ai.ts
|
||||||
|
import { createAI } from "@wrnexus/ai";
|
||||||
|
const ai = createAI(); // reads ANTHROPIC_API_KEY; default model claude-opus-4-8
|
||||||
|
export const POST = async (ctx) => {
|
||||||
|
const { prompt } = await ctx.req.json();
|
||||||
|
return ai.streamResponse(prompt); // or: return Response.json({ text: await ai.generate(prompt) })
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
```
|
||||||
|
wrnexus dev . # dev server + HMR
|
||||||
|
wrnexus build . # production build → dist/server.js
|
||||||
|
bun dist/server.js # run the production server (or npm start)
|
||||||
|
wrnexus create <name> # scaffold a new app
|
||||||
|
wrnexus update --latest # deps + syntax/config migrations + verification
|
||||||
|
wrnexus generate page <Name> # scaffold a page (aliases: g p)
|
||||||
|
wrnexus generate component <name> | api <path> | schema <name>
|
||||||
|
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
|
||||||
|
wrnexus eject <component> # copy a WrNexus UI component's .wrn into app/components to customize
|
||||||
|
```
|
||||||
|
|
||||||
|
## When asked to "create a page/component/feature"
|
||||||
|
|
||||||
|
1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page <Name>`.
|
||||||
|
2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`.
|
||||||
|
3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`.
|
||||||
|
4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wrn-*)`), or `style { }`.
|
||||||
|
5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineEndpoint } from "@wrnexus/core";
|
||||||
|
import { SearchDirectorySchema } from "../schemas/search-directory.ts";
|
||||||
|
|
||||||
|
interface DirectoryUser {
|
||||||
|
name: string;
|
||||||
|
designation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALL: DirectoryUser[] = [
|
||||||
|
{ name: "Ajay", designation: "UI" },
|
||||||
|
{ name: "Asha", designation: "Backend" },
|
||||||
|
{ name: "Chen", designation: "UI" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema-typed handler: input resolves to `{ name?: string }` from
|
||||||
|
* SearchDirectorySchema, so the block's request shape is checked for real
|
||||||
|
* (not against `unknown`, which the untyped-handler shape resolved to).
|
||||||
|
*/
|
||||||
|
export const POST = defineEndpoint<{ name?: string }, { users: DirectoryUser[] }>({
|
||||||
|
input: SearchDirectorySchema,
|
||||||
|
description: "Case-insensitive substring search over the demo directory by name.",
|
||||||
|
handler(input) {
|
||||||
|
const needle = String(input.name ?? "").toLowerCase();
|
||||||
|
return { users: ALL.filter((user) => user.name.toLowerCase().includes(needle)) };
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// AUTO-GENERATED by `wrnexus db generate` — do not edit.
|
// AUTO-GENERATED by `wrnexus db generate` (dialect: sqlite) — do not edit.
|
||||||
import type { Db, ExecResult } from "@wrnexus/db";
|
import type { Db, ExecResult } from "@wrnexus/db";
|
||||||
import { users } from "./schema.ts";
|
import { users } from "./schema.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
page ApiBlockDemo {
|
||||||
|
state nameFilter = "a"
|
||||||
|
state found = ""
|
||||||
|
state failed = ""
|
||||||
|
|
||||||
|
apis {
|
||||||
|
searchDirectory POST /api/directory {
|
||||||
|
request {
|
||||||
|
body {
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response {
|
||||||
|
return data.data.users
|
||||||
|
}
|
||||||
|
|
||||||
|
error {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
load server serverSearch {
|
||||||
|
const users = await api.searchDirectory({ name: "a" })
|
||||||
|
return { names: users.map((user) => user.name).join(", ") }
|
||||||
|
}
|
||||||
|
|
||||||
|
functions {
|
||||||
|
client async function search(): Promise<void> {
|
||||||
|
const users = await api.searchDirectory({ name: nameFilter })
|
||||||
|
found = users.map((user) => user.name).join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<main>
|
||||||
|
<button @click="search()">Search</button>
|
||||||
|
<p class="found" data-text="found">{found}</p>
|
||||||
|
<p class="failed" data-text="failed">{failed}</p>
|
||||||
|
<Async source="serverSearch">
|
||||||
|
<Loading><p class="server-found">Loading…</p></Loading>
|
||||||
|
<Success data="serverSearch"><p class="server-found">{serverSearch.names}</p></Success>
|
||||||
|
<Error error="error"><p class="server-found">Failed: {error.message}</p></Error>
|
||||||
|
</Async>
|
||||||
|
<p class="bound" api="searchDirectory({ name: 'a' })">loading</p>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,37 +18,32 @@ page Hello {
|
|||||||
canonical = "/hello"
|
canonical = "/hello"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSR data bindings run on the server before the HTML is sent.
|
// API calls used by this page. `ssrUsers` is bound in the initial render
|
||||||
// The API itself lives in app/api/users/ssr.ts; this block only calls it
|
// (its endpoint lives in app/api/users/ssr.ts); `csrUsers` is bound after
|
||||||
// and renders the response into HTML.
|
// hydration in the browser (app/api/users/csr.ts). Both just call the same
|
||||||
ssr {
|
// users endpoint shape, so the response handling looks the same.
|
||||||
functions {
|
apis {
|
||||||
function userNames(users) {
|
ssrUsers GET /api/users/ssr {
|
||||||
return users.map((user) => user.name).join(", ")
|
response {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api ssrUsers GET /api/users/ssr {
|
|
||||||
const visits = Number(cookies.get("hello_visits") ?? "0") + 1
|
const visits = Number(cookies.get("hello_visits") ?? "0") + 1
|
||||||
cookies.set("hello_visits", String(visits), { sameSite: "Lax" })
|
cookies.set("hello_visits", String(visits), { sameSite: "Lax" })
|
||||||
session.set("lastHelloVisit", visits)
|
session.set("lastHelloVisit", visits)
|
||||||
return `${userNames(users)} - visit ${visits}`
|
return `${userNames(data.users)} - visit ${visits}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client data bindings hydrate after the first paint. The browser only sees
|
csrUsers GET /api/users/csr {
|
||||||
// an opaque data-wrnexus-csr id; WrNexus calls app/api/users/csr.ts on the server.
|
response {
|
||||||
client {
|
|
||||||
functions {
|
|
||||||
function userNames(users) {
|
|
||||||
return users.map((user) => user.name).join(", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api csrUsers GET /api/users/csr {
|
|
||||||
const label = localStorage.get("wrnexus.label") ?? "browser"
|
const label = localStorage.get("wrnexus.label") ?? "browser"
|
||||||
session.set("lastClientLabel", label)
|
session.set("lastClientLabel", label)
|
||||||
return `${userNames(users)} - ${label}`
|
return `${userNames(data.users)} - ${label}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
functions {
|
||||||
|
shared function userNames(users) {
|
||||||
|
return users.map((user) => user.name).join(", ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
import { defineJob } from "@wrnexus/queue";
|
import { defineQueue } from "@wrnexus/queue";
|
||||||
|
|
||||||
export interface WelcomeEmailPayload {
|
export interface WelcomeEmailPayload {
|
||||||
userId: number;
|
userId: number;
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineJob<WelcomeEmailPayload>({
|
/*
|
||||||
|
* A file under app/queues must default-export defineQueue(...) -- the loader
|
||||||
|
* refuses anything else rather than registering a queue that would never run.
|
||||||
|
* This example used the older defineJob() shape and so failed to load, which
|
||||||
|
* took the whole example app down with it.
|
||||||
|
*/
|
||||||
|
export default defineQueue({
|
||||||
name: "welcome-email",
|
name: "welcome-email",
|
||||||
async run(job) {
|
jobs: {
|
||||||
console.log(`Welcome email queued for ${job.data.email}`);
|
send: {
|
||||||
|
validate: (data: unknown): data is WelcomeEmailPayload =>
|
||||||
|
typeof data === "object" &&
|
||||||
|
data !== null &&
|
||||||
|
Number.isInteger((data as { userId?: unknown }).userId) &&
|
||||||
|
typeof (data as { email?: unknown }).email === "string",
|
||||||
|
run: async ({ email }: WelcomeEmailPayload) => {
|
||||||
|
console.log(`Welcome email queued for ${email}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
export interface Routes {
|
export interface Routes {
|
||||||
"/": Record<string, never>;
|
"/": Record<string, never>;
|
||||||
"/about": Record<string, never>;
|
"/about": Record<string, never>;
|
||||||
|
"/api-block-demo": Record<string, never>;
|
||||||
"/async-data": Record<string, never>;
|
"/async-data": Record<string, never>;
|
||||||
"/chat": Record<string, never>;
|
"/chat": Record<string, never>;
|
||||||
"/client-only": Record<string, never>;
|
"/client-only": Record<string, never>;
|
||||||
@@ -27,6 +28,7 @@ export interface Routes {
|
|||||||
export interface RouteNames {
|
export interface RouteNames {
|
||||||
"index": "/";
|
"index": "/";
|
||||||
"about": "/about";
|
"about": "/about";
|
||||||
|
"api.block.demo": "/api-block-demo";
|
||||||
"async.data": "/async-data";
|
"async.data": "/async-data";
|
||||||
"chat": "/chat";
|
"chat": "/chat";
|
||||||
"client.only": "/client-only";
|
"client.only": "/client-only";
|
||||||
@@ -119,6 +121,7 @@ export function route<N extends RouteName>(
|
|||||||
const paths: Record<RouteName, RoutePath> = {
|
const paths: Record<RouteName, RoutePath> = {
|
||||||
"index": "/",
|
"index": "/",
|
||||||
"about": "/about",
|
"about": "/about",
|
||||||
|
"api.block.demo": "/api-block-demo",
|
||||||
"async.data": "/async-data",
|
"async.data": "/async-data",
|
||||||
"chat": "/chat",
|
"chat": "/chat",
|
||||||
"client.only": "/client-only",
|
"client.only": "/client-only",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { v } from "@wrnexus/validation";
|
||||||
|
|
||||||
|
export const SearchDirectorySchema = v.object({
|
||||||
|
name: v.string().trim().optional(),
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
|
||||||
|
//
|
||||||
|
// Type-only assertions for sectioned `api` blocks. Kept as a real .ts file (not
|
||||||
|
// wrnexus.generated.d.ts) because `skipLibCheck` exempts .d.ts contents from being
|
||||||
|
// checked; this file is compiled and checked normally by the project's own tsc.
|
||||||
|
export type __wrn_api_check_1fljm5i_searchDirectory = WRNexusGenerated.__wrn_expect_true<WRNexusGenerated.AssertAssignable<{ name?: string }, WRNexusGenerated.ApiInput<"/api/directory", "POST">>>;
|
||||||
|
export {};
|
||||||
+13
-2
@@ -13,8 +13,8 @@ declare namespace WRNexusGenerated {
|
|||||||
: never;
|
: never;
|
||||||
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
|
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
|
||||||
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
|
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
|
||||||
type RouteName = "about" | "async.data" | "chat" | "client.only" | "dashboard" | "hello" | "index" | "island.demo" | "language.tools" | "layout" | "login" | "modal" | "navigation" | "partial.static" | "platform.showcase" | "reactive" | "server.actions" | "table" | "test" | "ui";
|
type RouteName = "about" | "api.block.demo" | "async.data" | "chat" | "client.only" | "dashboard" | "hello" | "index" | "island.demo" | "language.tools" | "layout" | "login" | "modal" | "navigation" | "partial.static" | "platform.showcase" | "reactive" | "server.actions" | "table" | "test" | "ui";
|
||||||
type ApiRoute = "/api/accounts" | "/api/echo" | "/api/graphql-example" | "/api/hello" | "/api/invite" | "/api/login" | "/api/logout" | "/api/me" | "/api/typed-user" | "/api/users/csr" | "/api/users/ssr" | "/api/webhooks/payment";
|
type ApiRoute = "/api/accounts" | "/api/directory" | "/api/echo" | "/api/graphql-example" | "/api/hello" | "/api/invite" | "/api/login" | "/api/logout" | "/api/me" | "/api/typed-user" | "/api/users/csr" | "/api/users/ssr" | "/api/webhooks/payment";
|
||||||
type RealtimeRoute = "/realtime/chat" | "/realtime/hello";
|
type RealtimeRoute = "/realtime/chat" | "/realtime/hello";
|
||||||
type EnvironmentKey = "APP_LABEL" | "DATABASE_URL" | "DEMO_SHARED" | "HOST" | "NODE_ENV" | "PORT" | "SESSION_SECRET" | "UAT_ONLY";
|
type EnvironmentKey = "APP_LABEL" | "DATABASE_URL" | "DEMO_SHARED" | "HOST" | "NODE_ENV" | "PORT" | "SESSION_SECRET" | "UAT_ONLY";
|
||||||
type TranslationKey = "api.greeting" | "home.intro" | "home.title" | "nav.about" | "nav.chat" | "nav.dashboard" | "nav.home" | "nav.layout" | "nav.navigation" | "nav.ui";
|
type TranslationKey = "api.greeting" | "home.intro" | "home.title" | "nav.about" | "nav.chat" | "nav.dashboard" | "nav.home" | "nav.layout" | "nav.navigation" | "nav.ui";
|
||||||
@@ -29,6 +29,7 @@ declare namespace WRNexusGenerated {
|
|||||||
"/api/users/ssr": { GET: ApiContract<typeof import("../api/users/ssr.ts")["GET"]> };
|
"/api/users/ssr": { GET: ApiContract<typeof import("../api/users/ssr.ts")["GET"]> };
|
||||||
"/api/graphql-example": { POST: ApiContract<typeof import("../api/graphql-example.ts")["POST"]> };
|
"/api/graphql-example": { POST: ApiContract<typeof import("../api/graphql-example.ts")["POST"]> };
|
||||||
"/api/typed-user": { POST: ApiContract<typeof import("../api/typed-user.ts")["POST"]> };
|
"/api/typed-user": { POST: ApiContract<typeof import("../api/typed-user.ts")["POST"]> };
|
||||||
|
"/api/directory": { POST: ApiContract<typeof import("../api/directory.ts")["POST"]> };
|
||||||
"/api/accounts": { GET: ApiContract<typeof import("../api/accounts.ts")["GET"]> };
|
"/api/accounts": { GET: ApiContract<typeof import("../api/accounts.ts")["GET"]> };
|
||||||
"/api/invite": { POST: ApiContract<typeof import("../api/invite.ts")["POST"]> };
|
"/api/invite": { POST: ApiContract<typeof import("../api/invite.ts")["POST"]> };
|
||||||
"/api/logout": { POST: ApiContract<typeof import("../api/logout.ts")["POST"]> };
|
"/api/logout": { POST: ApiContract<typeof import("../api/logout.ts")["POST"]> };
|
||||||
@@ -57,4 +58,14 @@ declare namespace WRNexusGenerated {
|
|||||||
"welcome-email": QueuePayload<(typeof import("../queues/welcome-email.ts"))["default"]>;
|
"welcome-email": QueuePayload<(typeof import("../queues/welcome-email.ts"))["default"]>;
|
||||||
}
|
}
|
||||||
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
|
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
|
||||||
|
type AssertAssignable<Actual, Expected> = unknown extends Expected
|
||||||
|
? true
|
||||||
|
: [Actual] extends [Expected]
|
||||||
|
? [Exclude<keyof Actual, keyof Expected>] extends [never]
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false;
|
||||||
|
type __wrn_expect_true<T extends true> = T;
|
||||||
|
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
|
||||||
|
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "basic-app",
|
"name": "basic-app",
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
|
"wrnexus": {
|
||||||
|
"version": "0.9.0"
|
||||||
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
# WrNexus
|
||||||
|
|
||||||
|
> WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in
|
||||||
|
> `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based.
|
||||||
|
> This document teaches an AI how to write correct WrNexus code. It is private and
|
||||||
|
> post-dates model training data, so rely on THIS document, not prior web-framework
|
||||||
|
> assumptions.
|
||||||
|
|
||||||
|
## Golden rules
|
||||||
|
|
||||||
|
- **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React
|
||||||
|
for UI. Do NOT use `useState`, hooks, JSX, or a client bundler.
|
||||||
|
- **Routing is file-based** under `app/`. The filename is the route. No router config.
|
||||||
|
- **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render
|
||||||
|
on the server and hydrate automatically — you never write client-side JS islands.
|
||||||
|
- **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported.
|
||||||
|
- To add files, prefer the CLI: `wrnexus generate page <Name>` / `component <name>` / `api <path>` / `schema <name>`.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
pages/ *.wrn → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug"
|
||||||
|
components/ *.wrn → reusable UI, mounted in a page/component via <div data-component="name" ...props>
|
||||||
|
layouts/ *.wrn → named layouts; a page opts in with layout = "name"
|
||||||
|
api/ *.ts → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response
|
||||||
|
middleware/ *.ts → export default async (ctx, next) => next()
|
||||||
|
realtime/ *.ts → export default defineRoom({ ... }) from "@wrnexus/core" (ws://host/realtime/<name>)
|
||||||
|
schemas/ *.ts → validation schemas (the `v` builder), used by forms + parseBody
|
||||||
|
locales/ *.json → i18n messages per language
|
||||||
|
db/ schema.ts, queries/*.sql, migrations/*.sql
|
||||||
|
styles/ global.css → Tailwind (default) or plain CSS
|
||||||
|
wrnexus.config.ts → app config (AppConfig from "@wrnexus/styles")
|
||||||
|
public/ → static assets served at /
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.wrn` page
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
page Home {
|
||||||
|
layout = "public" // optional: a component in app/layouts/<name>.wrn ("none" to skip)
|
||||||
|
|
||||||
|
state count = 0 // optional: seeds client-reactive state (omit for pure SSR)
|
||||||
|
|
||||||
|
seo {
|
||||||
|
title = "Home"
|
||||||
|
description = "..."
|
||||||
|
canonical = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<h1>Hello</h1>
|
||||||
|
<p>Count is {count}, doubled is {count * 2}.</p>
|
||||||
|
<button @click="count++">Increment</button>
|
||||||
|
<div data-component="counter" start="5" label="Clicks"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
style {
|
||||||
|
h1 { color: var(--wrn-color-text); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `.wrn` component
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
component Counter {
|
||||||
|
props { // props come from mount attributes; each is coerced to the
|
||||||
|
start = 0 // TYPE of its default (so start="5" arrives as the number 5)
|
||||||
|
label = "Count"
|
||||||
|
}
|
||||||
|
state count = start // state may reference props
|
||||||
|
view {
|
||||||
|
<button @click="count++">{label}: {count}</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Mount it from any page/component: `<div data-component="counter" start="0" label="Clicks"></div>`.
|
||||||
|
Components render on the server with their props, then hydrate — no per-component JS.
|
||||||
|
|
||||||
|
## The `view { }` block (plain HTML + a few directives)
|
||||||
|
|
||||||
|
- `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`.
|
||||||
|
- `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`.
|
||||||
|
- `<div data-component="name" prop="v">` — mount a component (attrs become string props, coerced).
|
||||||
|
- `<slot></slot>` / `<slot name="x"></slot>` — component/layout slots; fill with `<div data-slot="x">…</div>`.
|
||||||
|
- **Server loop (DB/list/table):** `{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `<list>` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`.
|
||||||
|
- **Server conditional:** `{#if <expr>} … {:else if <expr>} … {:else} … {/if}` — renders the first truthy branch on the server. `<expr>` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}<span>●</span>{:else}<span>○</span>{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead.
|
||||||
|
- i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`.
|
||||||
|
- Theme: any element with `data-wrn-theme-toggle` toggles light/dark; `data-wrn-theme-set="dark"` sets it.
|
||||||
|
- Void/self-closing tags are fine: `<br />`, `<img src="..." />`.
|
||||||
|
- Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text.
|
||||||
|
|
||||||
|
## Data-driven tables / lists (server-rendered `.wrn`)
|
||||||
|
|
||||||
|
Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them.
|
||||||
|
This renders on the **server** (SSR-first) and is HTML-escaped by default.
|
||||||
|
|
||||||
|
```wrn
|
||||||
|
page Admin {
|
||||||
|
layout = "dashboard"
|
||||||
|
|
||||||
|
// Fetch on the server. The api handler at /api/contacts returns { contacts: [...] };
|
||||||
|
// this block's `return contacts` exposes that array (via `$data`) as the binding `rows`.
|
||||||
|
ssr {
|
||||||
|
api rows GET /api/contacts { return contacts }
|
||||||
|
}
|
||||||
|
|
||||||
|
view {
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{#each rows as r, i}
|
||||||
|
<tr>
|
||||||
|
<td>#{i}</td>
|
||||||
|
<td>{r.name}</td>
|
||||||
|
<td><a href="mailto:{r.email}">{r.email}</a></td>
|
||||||
|
</tr>
|
||||||
|
{:empty}
|
||||||
|
<tr><td colspan="3">No submissions yet.</td></tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The matching API returns the array under a key the `ssr` block reads:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/api/contacts.ts → GET /api/contacts
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
export const GET = async () => {
|
||||||
|
const contacts = await getDb().all("SELECT id, name, email FROM contacts ORDER BY id DESC");
|
||||||
|
return Response.json({ contacts }); // ssr block does `return contacts`
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prefer this `.wrn` + `{#each}` approach for DB-backed tables and lists.** (`.ts`/`.tsx`
|
||||||
|
pages returning an HTML string are also supported for fully-custom programmatic rendering,
|
||||||
|
but a `.wrn` page with `ssr` data + `{#each}` is the idiomatic, SSR-first way.)
|
||||||
|
|
||||||
|
## API routes (`app/api/*.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/api/users/list.ts → GET /api/users/list
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
|
||||||
|
export const GET = async (ctx) => {
|
||||||
|
return Response.json({ users: await ListUsers(getDb()) });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = async (ctx) => {
|
||||||
|
const body = await ctx.req.json();
|
||||||
|
return Response.json({ ok: true, body }, { status: 201 });
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`ctx` (the `Context` from `@wrnexus/core`) has:
|
||||||
|
`req: Request`, `url: URL`, `params: Record<string,string>` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`),
|
||||||
|
`lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`.
|
||||||
|
|
||||||
|
When an SSO forward-auth verifier needs the URL that originally reached the gateway, use
|
||||||
|
`@wrnexus/helpers` instead of constructing it from untrusted headers:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { redirectToLogin } from "@wrnexus/helpers";
|
||||||
|
|
||||||
|
return redirectToLogin(ctx, "/login", {
|
||||||
|
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The package also exports `getOriginalRequestUrl`, `getOriginalRequestOrigin`,
|
||||||
|
`getOriginalRequestPath`, and `getOriginalRequestMethod`. Always pass `allowedHosts` when
|
||||||
|
using forwarded gateway URLs; the helper rejects untrusted redirect destinations.
|
||||||
|
|
||||||
|
## Middleware & realtime
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/middleware/logger.ts
|
||||||
|
export default async function logger(ctx, next) {
|
||||||
|
console.log(ctx.req.method, ctx.url.pathname);
|
||||||
|
return next(); // return a Response WITHOUT calling next() to short-circuit
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/realtime/chat.ts → ws://host/realtime/chat
|
||||||
|
import { defineRoom } from "@wrnexus/core";
|
||||||
|
export default defineRoom({
|
||||||
|
onConnect(client) { client.send({ type: "system", text: "connected" }); },
|
||||||
|
onMessage(client, msg) { client.room.broadcast({ type: "message", data: msg }); },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
Client side: a page opts in with `data-room="chat"` (handled by the realtime runtime).
|
||||||
|
|
||||||
|
## Config (`wrnexus.config.ts`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { AppConfig } from "@wrnexus/styles";
|
||||||
|
const config: AppConfig = {
|
||||||
|
seo: { title: "App", titleTemplate: "%s | App", description: "..." },
|
||||||
|
styles: { entry: "app/styles/global.css", process: async ({ entryPath, mode }) => /* Tailwind */ "" },
|
||||||
|
fonts: { sans: '"Inter", system-ui, sans-serif', google: [{ family: "Inter", weights: [400, 600] }] },
|
||||||
|
theme: { default: "dark", themes: { light: { "color-primary": "#2563eb" } } },
|
||||||
|
i18n: { default: "en", locales: ["en", "es"] },
|
||||||
|
db: { driver: "sqlite", url: "file:./dev.db" },
|
||||||
|
security: { cors: { enabled: true, origin: ["http://localhost:5173"] } },
|
||||||
|
// profiles: { production: { db: { driver: "postgres", url: process.env.DATABASE_URL } } },
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database (`@wrnexus/db`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/db/schema.ts
|
||||||
|
import { v, table } from "@wrnexus/db";
|
||||||
|
export const users = table("users", {
|
||||||
|
id: v.id(),
|
||||||
|
name: v.string(),
|
||||||
|
email: v.string().unique(),
|
||||||
|
createdAt: v.timestamp(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
- Queries: write `app/db/queries/*.sql` with `-- name: ListUsers :many` blocks; `wrnexus db generate` emits typed functions.
|
||||||
|
- Access at runtime: `import { getDb } from "@wrnexus/db"; const rows = await ListUsers(getDb());`
|
||||||
|
- Migrations in `app/db/migrations/`; run `wrnexus db migrate` (dev auto-migrates sqlite).
|
||||||
|
|
||||||
|
## Validation (`@wrnexus/validation`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/schemas/login.ts
|
||||||
|
import { v } from "@wrnexus/validation";
|
||||||
|
export default v.object({
|
||||||
|
email: v.string().email(),
|
||||||
|
password: v.string().min(8),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
In an API route: `import s from "../schemas/login"; import { parseBody } from "@wrnexus/validation"; const r = await parseBody(s, ctx.req);` → `r.ok ? r.value : r.response`.
|
||||||
|
In a form: `<form data-schema="login" action="/api/login" method="post">` + `<span data-error="email"></span>` (client + server validation connected automatically).
|
||||||
|
|
||||||
|
## AI / LLM (`@wrnexus/ai`)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// app/api/ai.ts
|
||||||
|
import { createAI } from "@wrnexus/ai";
|
||||||
|
const ai = createAI(); // reads ANTHROPIC_API_KEY; default model claude-opus-4-8
|
||||||
|
export const POST = async (ctx) => {
|
||||||
|
const { prompt } = await ctx.req.json();
|
||||||
|
return ai.streamResponse(prompt); // or: return Response.json({ text: await ai.generate(prompt) })
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
```
|
||||||
|
wrnexus dev . # dev server + HMR
|
||||||
|
wrnexus build . # production build → dist/server.js
|
||||||
|
bun dist/server.js # run the production server (or npm start)
|
||||||
|
wrnexus create <name> # scaffold a new app
|
||||||
|
wrnexus update --latest # deps + syntax/config migrations + verification
|
||||||
|
wrnexus generate page <Name> # scaffold a page (aliases: g p)
|
||||||
|
wrnexus generate component <name> | api <path> | schema <name>
|
||||||
|
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
|
||||||
|
wrnexus eject <component> # copy a WrNexus UI component's .wrn into app/components to customize
|
||||||
|
```
|
||||||
|
|
||||||
|
## When asked to "create a page/component/feature"
|
||||||
|
|
||||||
|
1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page <Name>`.
|
||||||
|
2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`.
|
||||||
|
3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`.
|
||||||
|
4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wrn-*)`), or `style { }`.
|
||||||
|
5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.
|
||||||
@@ -13,8 +13,6 @@ import type { AppConfig } from "@wrnexus/styles";
|
|||||||
* framework-level headers and optional CORS.
|
* framework-level headers and optional CORS.
|
||||||
*/
|
*/
|
||||||
const config: AppConfig = {
|
const config: AppConfig = {
|
||||||
frameworkBehaviour: 1,
|
|
||||||
compatibilityDate: "2026-08-02",
|
|
||||||
head: [
|
head: [
|
||||||
// --- Use a CSS framework via CDN (uncomment one) ---
|
// --- Use a CSS framework via CDN (uncomment one) ---
|
||||||
// Bootstrap:
|
// Bootstrap:
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
dist/
|
||||||
|
.wrnexus/
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Northstar CRM example
|
||||||
|
|
||||||
|
A complete WrNexus example covering public pages, SQLite migrations and seed data, SQL-backed signup/login/session authentication, database-backed roles and permissions, protected pages, and permission-checked CRM APIs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:migrate
|
||||||
|
bun run db:seed
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `/signup`, create an account, and the signup hook assigns the `sales-rep` role. The user is automatically signed in and redirected to `/dashboard`. Auth tables come from the `@wrnexus/auth` plugin migrations; CRM and authorization tables come from `app/db/migrations/001_crm.sql`.
|
||||||
|
|
||||||
|
The seed records use the placeholder owner `demo-owner` to demonstrate repeatable data. To attach them to a registered user, update their `owner_id` to that user's ID.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { can } from "@wrnexus/authz";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { ensureCrmWorkspace } from "../lib/workspace.ts";
|
||||||
|
|
||||||
|
const userId = (ctx: Context) => String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
|
||||||
|
|
||||||
|
export async function GET(ctx: Context): Promise<Response> {
|
||||||
|
if (!(await can(ctx, "contact:read")))
|
||||||
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
await ensureCrmWorkspace(userId(ctx));
|
||||||
|
const rows = await getDb().all(
|
||||||
|
"SELECT id,name,email,company,phone,status,created_at FROM crm_contacts WHERE owner_id = ? ORDER BY name",
|
||||||
|
[userId(ctx)],
|
||||||
|
);
|
||||||
|
return Response.json({ contacts: rows });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(ctx: Context): Promise<Response> {
|
||||||
|
if (!(await can(ctx, "contact:write")))
|
||||||
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
const body = (await ctx.req.json()) as Record<string, unknown>;
|
||||||
|
const name = String(body.name ?? "").trim();
|
||||||
|
const email = String(body.email ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (!name || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
return Response.json({ error: "A name and valid email are required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const result = await getDb().exec(
|
||||||
|
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[userId(ctx), name, email, String(body.company ?? ""), String(body.phone ?? ""), "lead"],
|
||||||
|
);
|
||||||
|
return Response.json({ id: Number(result.lastInsertId), name, email }, { status: 201 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { can } from "@wrnexus/authz";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { ensureCrmWorkspace } from "../lib/workspace.ts";
|
||||||
|
|
||||||
|
export async function GET(ctx: Context): Promise<Response> {
|
||||||
|
if (!(await can(ctx, "crm:dashboard")))
|
||||||
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
const owner = String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
|
||||||
|
await ensureCrmWorkspace(owner);
|
||||||
|
const metrics = await getDb().one<{
|
||||||
|
pipeline_value_cents: number;
|
||||||
|
open_opportunities: number;
|
||||||
|
contacts: number;
|
||||||
|
}>(
|
||||||
|
`SELECT
|
||||||
|
COALESCE((SELECT SUM(value_cents) FROM crm_deals WHERE owner_id = ? AND stage NOT IN ('won','lost')), 0) pipeline_value_cents,
|
||||||
|
(SELECT COUNT(*) FROM crm_deals WHERE owner_id = ? AND stage NOT IN ('won','lost')) open_opportunities,
|
||||||
|
(SELECT COUNT(*) FROM crm_contacts WHERE owner_id = ?) contacts`,
|
||||||
|
[owner, owner, owner],
|
||||||
|
);
|
||||||
|
return Response.json({
|
||||||
|
metrics: {
|
||||||
|
pipelineValue: new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(Number(metrics?.pipeline_value_cents ?? 0) / 100),
|
||||||
|
openOpportunities: Number(metrics?.open_opportunities ?? 0),
|
||||||
|
contacts: Number(metrics?.contacts ?? 0),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { can } from "@wrnexus/authz";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { ensureCrmWorkspace } from "../lib/workspace.ts";
|
||||||
|
|
||||||
|
export async function GET(ctx: Context): Promise<Response> {
|
||||||
|
if (!(await can(ctx, "deal:read"))) return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
const owner = String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
|
||||||
|
await ensureCrmWorkspace(owner);
|
||||||
|
const deals = await getDb().all(
|
||||||
|
"SELECT d.id,d.title,d.value_cents,d.stage,d.close_date,c.name contact_name FROM crm_deals d LEFT JOIN crm_contacts c ON c.id=d.contact_id WHERE d.owner_id=? ORDER BY d.updated_at DESC",
|
||||||
|
[owner],
|
||||||
|
);
|
||||||
|
return Response.json({ deals });
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineAuthz } from "@wrnexus/authz";
|
||||||
|
|
||||||
|
export default defineAuthz({
|
||||||
|
permissions: {
|
||||||
|
"crm:dashboard": { title: "View dashboard" },
|
||||||
|
"contact:read": { title: "View contacts" },
|
||||||
|
"contact:write": { title: "Create and edit contacts" },
|
||||||
|
"contact:delete": { title: "Delete contacts", risk: "high" },
|
||||||
|
"deal:read": { title: "View deals" },
|
||||||
|
"deal:write": { title: "Create and edit deals" },
|
||||||
|
"admin:access": { title: "Manage CRM access", risk: "high" },
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
viewer: ["crm:dashboard", "contact:read", "deal:read"],
|
||||||
|
"sales-rep": ["role:viewer", "contact:write", "deal:write"],
|
||||||
|
manager: ["role:sales-rep", "contact:delete"],
|
||||||
|
admin: ["role:manager", "admin:access"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
-- +up
|
||||||
|
CREATE TABLE IF NOT EXISTS crm_contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
company TEXT NOT NULL DEFAULT '',
|
||||||
|
phone TEXT NOT NULL DEFAULT '',
|
||||||
|
status TEXT NOT NULL DEFAULT 'lead' CHECK (status IN ('lead', 'customer', 'inactive')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS crm_contacts_owner_idx ON crm_contacts(owner_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS crm_contacts_owner_email_uq ON crm_contacts(owner_id, email);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS crm_deals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_id TEXT NOT NULL,
|
||||||
|
contact_id INTEGER REFERENCES crm_contacts(id) ON DELETE SET NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
value_cents INTEGER NOT NULL DEFAULT 0 CHECK (value_cents >= 0),
|
||||||
|
stage TEXT NOT NULL DEFAULT 'qualified' CHECK (stage IN ('qualified', 'proposal', 'won', 'lost')),
|
||||||
|
close_date TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS crm_deals_owner_idx ON crm_deals(owner_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS crm_deals_contact_idx ON crm_deals(contact_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS crm_activities (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
actor_id TEXT NOT NULL,
|
||||||
|
entity_type TEXT NOT NULL CHECK (entity_type IN ('contact', 'deal', 'account')),
|
||||||
|
entity_id TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
details_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS crm_activities_entity_idx ON crm_activities(entity_type, entity_id);
|
||||||
|
|
||||||
|
-- Database-backed authorization assignments.
|
||||||
|
CREATE TABLE IF NOT EXISTS _wrn_authz_assignment (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
subject_id VARCHAR(255) NOT NULL,
|
||||||
|
scope VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
role VARCHAR(255) NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT _wrn_authz_assignment_unique UNIQUE (subject_id, scope, role)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS _wrn_authz_grant (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
subject_id VARCHAR(255) NOT NULL,
|
||||||
|
scope VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
permission VARCHAR(255) NOT NULL,
|
||||||
|
effect VARCHAR(16) NOT NULL CHECK (effect IN ('allow', 'deny')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT _wrn_authz_grant_unique UNIQUE (subject_id, scope, permission)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +down
|
||||||
|
DROP TABLE IF EXISTS _wrn_authz_grant;
|
||||||
|
DROP TABLE IF EXISTS _wrn_authz_assignment;
|
||||||
|
DROP TABLE IF EXISTS crm_activities;
|
||||||
|
DROP TABLE IF EXISTS crm_deals;
|
||||||
|
DROP TABLE IF EXISTS crm_contacts;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Db } from "@wrnexus/db";
|
||||||
|
|
||||||
|
export default async function seed(db: Db): Promise<void> {
|
||||||
|
const owner = "demo-owner";
|
||||||
|
await db.exec("DELETE FROM crm_activities");
|
||||||
|
await db.exec("DELETE FROM crm_deals");
|
||||||
|
await db.exec("DELETE FROM crm_contacts");
|
||||||
|
await db.exec(
|
||||||
|
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[owner, "Ada Lovelace", "ada@example.test", "Analytical Engines", "+1 555 0101", "customer"],
|
||||||
|
);
|
||||||
|
await db.exec(
|
||||||
|
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[owner, "Grace Hopper", "grace@example.test", "Compiler Labs", "+1 555 0102", "lead"],
|
||||||
|
);
|
||||||
|
const contact = await db.one<{ id: number }>("SELECT id FROM crm_contacts WHERE email = ?", [
|
||||||
|
"ada@example.test",
|
||||||
|
]);
|
||||||
|
await db.exec(
|
||||||
|
"INSERT INTO crm_deals (owner_id,contact_id,title,value_cents,stage,close_date) VALUES (?,?,?,?,?,?)",
|
||||||
|
[owner, contact?.id ?? null, "Enterprise rollout", 12500000, "proposal", "2026-12-15"],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createAuthEngine, SqlAuthStore, type AuthStore } from "@wrnexus/auth";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import { dbPermissionStore } from "@wrnexus/authz/db";
|
||||||
|
import { ensureCrmWorkspace } from "./workspace.ts";
|
||||||
|
|
||||||
|
// Config is imported before the runtime opens its configured database. This
|
||||||
|
// forwarding store resolves getDb() only when an auth operation actually runs.
|
||||||
|
const store = new Proxy({} as AuthStore, {
|
||||||
|
get(_target, property) {
|
||||||
|
const value = Reflect.get(new SqlAuthStore(getDb()), property);
|
||||||
|
return typeof value === "function" ? value.bind(new SqlAuthStore(getDb())) : value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const auth = createAuthEngine({
|
||||||
|
store,
|
||||||
|
secret: process.env.AUTH_SECRET ?? "northstar-crm-development-secret-change-me",
|
||||||
|
issuer: "Northstar CRM",
|
||||||
|
onSignedIn(ctx, returnTo) {
|
||||||
|
const destination =
|
||||||
|
returnTo?.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/dashboard";
|
||||||
|
return Response.redirect(new URL(destination, ctx.url), 303);
|
||||||
|
},
|
||||||
|
onSignedOut(ctx) {
|
||||||
|
return Response.redirect(new URL("/", ctx.url), 303);
|
||||||
|
},
|
||||||
|
async onSuccessfulSignUp(_ctx, user) {
|
||||||
|
await dbPermissionStore(getDb()).assignRole(user.id, "sales-rep");
|
||||||
|
await ensureCrmWorkspace(user.id);
|
||||||
|
return { autoSignIn: true, redirectTo: "/dashboard" };
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
|
||||||
|
/** Give every real CRM user a useful workspace on first access. */
|
||||||
|
export async function ensureCrmWorkspace(ownerId: string): Promise<void> {
|
||||||
|
if (!ownerId) return;
|
||||||
|
const db = getDb();
|
||||||
|
await db.exec(
|
||||||
|
"INSERT OR IGNORE INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[ownerId, "Ada Lovelace", "ada@example.test", "Analytical Engines", "+1 555 0101", "customer"],
|
||||||
|
);
|
||||||
|
await db.exec(
|
||||||
|
"INSERT OR IGNORE INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[ownerId, "Grace Hopper", "grace@example.test", "Compiler Labs", "+1 555 0102", "lead"],
|
||||||
|
);
|
||||||
|
const existingDeal = await db.one<{ count: number }>(
|
||||||
|
"SELECT COUNT(*) count FROM crm_deals WHERE owner_id = ?",
|
||||||
|
[ownerId],
|
||||||
|
);
|
||||||
|
if (Number(existingDeal?.count ?? 0) === 0) {
|
||||||
|
const contact = await db.one<{ id: number }>(
|
||||||
|
"SELECT id FROM crm_contacts WHERE owner_id = ? AND email = ?",
|
||||||
|
[ownerId, "ada@example.test"],
|
||||||
|
);
|
||||||
|
await db.exec(
|
||||||
|
"INSERT INTO crm_deals (owner_id,contact_id,title,value_cents,stage,close_date) VALUES (?,?,?,?,?,?)",
|
||||||
|
[ownerId, contact?.id ?? null, "Enterprise rollout", 12500000, "proposal", "2026-12-15"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import { dbPermissionStore } from "@wrnexus/authz/db";
|
||||||
|
import type { Middleware } from "@wrnexus/core";
|
||||||
|
|
||||||
|
// Production imports middleware before it opens configured databases. Resolve
|
||||||
|
// the SQL-backed store on the first request, after runtime initialization.
|
||||||
|
const middleware: Middleware = (ctx, next) =>
|
||||||
|
authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) })(ctx, next);
|
||||||
|
|
||||||
|
export default middleware;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { requireAuth } from "@wrnexus/auth";
|
||||||
|
import { can } from "@wrnexus/authz";
|
||||||
|
import type { Middleware } from "@wrnexus/core";
|
||||||
|
|
||||||
|
const guard = requireAuth({ loginPath: "/login" });
|
||||||
|
const protectedPrefixes = ["/dashboard", "/contacts", "/deals", "/admin"];
|
||||||
|
|
||||||
|
const middleware: Middleware = (ctx, next) => {
|
||||||
|
if (!protectedPrefixes.some((prefix) => ctx.url.pathname.startsWith(prefix))) return next();
|
||||||
|
return guard(ctx, async () => {
|
||||||
|
if (ctx.url.pathname.startsWith("/admin") && !(await can(ctx, "admin:access"))) {
|
||||||
|
return Response.redirect(new URL("/forbidden", ctx.url), 303);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default middleware;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
page Admin {
|
||||||
|
state user = ctx.user
|
||||||
|
seo { title = "Administration" }
|
||||||
|
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts">Contacts</a><a href="/deals">Deals</a><a href="/admin" aria-current="page">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Access administration</h1><p>Manage roles and protect high-risk operations.</p></div></header><section class="crm-panel"><span class="crm-eyebrow">Role-based access</span><h2>Signed in as {user.displayName || user.username}</h2><p class="crm-hero-copy">This route requires the <strong>admin:access</strong> permission. Assign manager and administrator roles through the database-backed authorization store.</p></section></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
page Contacts {
|
||||||
|
apis { listContacts GET /api/contacts { response { return data.contacts } } }
|
||||||
|
load server contacts { return await api.listContacts() }
|
||||||
|
seo { title = "Contacts" }
|
||||||
|
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts" aria-current="page">Contacts</a><a href="/deals">Deals</a><a href="/admin">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Contacts</h1><p>Every relationship, scoped securely to its owner.</p></div><a class="crm-button" href="mailto:sales@example.test">New contact</a></header><section class="crm-panel"><span class="crm-eyebrow">Customer directory</span>{#if contacts.length}<ul class="crm-list">{#each contacts as contact}<li><strong>{contact.name}</strong><span>{contact.company || contact.email}</span><span class="crm-pill">{contact.status}</span></li>{/each}</ul>{/if}{#if !contacts.length}<div class="crm-empty"><h2>No contacts yet</h2><p>Your first customer relationship will appear here.</p></div>{/if}</section></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
page Dashboard {
|
||||||
|
state user = ctx.user
|
||||||
|
state dashboardMetrics = ctx.metrics
|
||||||
|
apis { dashboard GET /api/dashboard { response { return data.metrics } } }
|
||||||
|
load server metrics { return await api.dashboard() }
|
||||||
|
seo { title = "Dashboard" }
|
||||||
|
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard" aria-current="page">Overview</a><a href="/contacts">Contacts</a><a href="/deals">Deals</a><a href="/admin">Administration</a></nav><form class="crm-sidebar-footer" method="post" action="/api/auth/logout" data-schema="auth-empty"><button class="crm-button crm-button--ghost" type="submit">Log out</button></form></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Good to see you, {user.displayName || user.username}</h1><p>Live figures from your private SQLite workspace.</p></div><a class="crm-button" href="/contacts">View contacts</a></header><div class="crm-stat-grid"><article class="crm-stat"><span>Pipeline value</span><strong>{dashboardMetrics.pipelineValue}</strong></article><article class="crm-stat"><span>Open opportunities</span><strong>{dashboardMetrics.openOpportunities}</strong></article><article class="crm-stat"><span>Total contacts</span><strong>{dashboardMetrics.contacts}</strong></article></div><section class="crm-panel"><span class="crm-eyebrow">Server rendered</span><h2>One secure data path</h2><p class="crm-hero-copy">This page was rendered from authenticated API results on the server. No browser prefetch or client data request is needed to show these metrics.</p></section></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
page Deals {
|
||||||
|
apis { listDeals GET /api/deals { response { return data.deals } } }
|
||||||
|
load server deals { return await api.listDeals() }
|
||||||
|
seo { title = "Deals" }
|
||||||
|
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts">Contacts</a><a href="/deals" aria-current="page">Deals</a><a href="/admin">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Sales pipeline</h1><p>Focus on the opportunities most likely to move.</p></div><a class="crm-button" href="/contacts">New deal</a></header><section class="crm-panel"><span class="crm-eyebrow">Open opportunities</span>{#if deals.length}<ul class="crm-list">{#each deals as deal}<li><strong>{deal.title}</strong><span>{deal.contact_name || "Unassigned contact"}</span><span class="crm-pill">{deal.stage}</span></li>{/each}</ul>{/if}{#if !deals.length}<div class="crm-empty"><h2>No open deals</h2><p>Create a contact and turn the relationship into an opportunity.</p></div>{/if}</section></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
page Forbidden {
|
||||||
|
seo { title = "Access denied" }
|
||||||
|
view {
|
||||||
|
<main class="crm-status-page"><section class="crm-status-card"><span class="crm-status-icon" aria-hidden="true">!</span><span class="crm-eyebrow">Permission required</span><h1>You do not have access to administration.</h1><p>Your account is working correctly, but an administrator role is required for this page.</p><div class="crm-actions"><a class="crm-button" href="/dashboard">Back to dashboard</a><a class="crm-button crm-button--ghost" href="/contacts">View contacts</a></div></section></main>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
page Home {
|
||||||
|
seo { title = "CRM that keeps sales moving" }
|
||||||
|
view {
|
||||||
|
<main
|
||||||
|
class="crm-public"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
class="crm-nav"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="crm-brand"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="crm-brand-mark"
|
||||||
|
>
|
||||||
|
N</span>Northstar CRM</a><div class="crm-nav-links"><a href="/pricing">Pricing</a><a href="/login">Log in</a><a class="crm-button" href="/signup">Start free</a></div>
|
||||||
|
</nav>
|
||||||
|
<section
|
||||||
|
class="crm-hero"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="crm-eyebrow"
|
||||||
|
>
|
||||||
|
Customer intelligence, simplified</span><h1>Know every customer. Close every opportunity.</h1><p class="crm-hero-copy">Bring contacts, conversations, and pipeline into one calm workspace built for teams that value clarity.</p><div class="crm-actions"><a class="crm-button" href="/signup">Create your workspace</a><a class="crm-button crm-button--ghost" href="/login">View the CRM</a></div></div><div class="crm-preview"><div class="crm-preview-bar"><span></span><span></span><span></span></div><div class="crm-preview-grid"><article class="crm-preview-card"><small>Pipeline value</small><strong>$125k</strong><small>+18% this month</small></article><article class="crm-preview-card"><small>Active contacts</small><strong>248</strong><small>32 need follow-up</small></article><article class="crm-preview-card"><small>Win rate</small><strong>42%</strong><small>Across qualified deals</small></article><article class="crm-preview-card"><small>Next action</small><strong>8 today</strong><small>Stay ahead of every promise</small></article></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import AuthSplitLayout from "@wrnexus/ui/components/AuthSplitLayout.wrn"
|
||||||
|
|
||||||
|
page Login {
|
||||||
|
seo { title = "Log in" }
|
||||||
|
view {
|
||||||
|
<AuthSplitLayout brand="Northstar CRM" eyebrow="Customer intelligence" title="Turn every conversation into momentum." description="Sign in to a focused workspace for contacts, pipeline, and activity." features='[{"label":"One customer timeline","description":"Every relationship and opportunity in context."},{"label":"Secure by default","description":"SQL sessions and role-based authorization."},{"label":"Built for focus","description":"A calm workspace without sales-tool clutter."}]'>
|
||||||
|
<div data-slot="form"><SignIn action="/api/auth/login" returnTo="/dashboard" signUpHref="/signup" forgotHref="/login" showPasskey="false" class="border-0 shadow-none" /></div>
|
||||||
|
</AuthSplitLayout>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
page Pricing {
|
||||||
|
seo { title = "Pricing" }
|
||||||
|
view { <main class="crm-public"><nav class="crm-nav"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><div class="crm-nav-links"><a href="/login">Log in</a><a class="crm-button" href="/signup">Start free</a></div></nav><section class="crm-hero"><div><span class="crm-eyebrow">Simple pricing</span><h1>Start free. Scale when your team does.</h1><p class="crm-hero-copy">Everything needed to evaluate a secure, full-stack CRM locally. Move to Team when you are ready to collaborate.</p></div><div class="crm-preview"><article class="crm-preview-card"><small>Starter</small><strong>$0</strong><p>SQLite workspace, CRM flows, authentication and permissions.</p><a class="crm-button" href="/signup">Start building</a></article><article class="crm-preview-card"><small>Team</small><strong>$29</strong><p>Per user/month with shared pipeline and administration.</p></article></div></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import AuthSplitLayout from "@wrnexus/ui/components/AuthSplitLayout.wrn"
|
||||||
|
|
||||||
|
page Signup {
|
||||||
|
seo { title = "Create account" }
|
||||||
|
view {
|
||||||
|
<AuthSplitLayout brand="Northstar CRM" eyebrow="Start in minutes" title="Build relationships, not spreadsheets." description="Create your secure sales workspace and begin with a complete customer view." features='[{"label":"Free local workspace","description":"Explore the complete CRM flow with SQLite."},{"label":"Flexible permissions","description":"Viewer, sales, manager, and admin roles."},{"label":"Your data stays yours","description":"Portable SQL data and explicit migrations."}]'>
|
||||||
|
<div data-slot="form"><SignUp redirect="/dashboard" signInHref="/login" showPhone="false" showUsername="false" requireConsent="false" class="border-0 shadow-none" /></div>
|
||||||
|
</AuthSplitLayout>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// AUTO-GENERATED by `wrnexus dev` - do not edit.
|
||||||
|
// Typed routes support required, optional, and catch-all parameters.
|
||||||
|
|
||||||
|
export interface Routes {
|
||||||
|
"/": Record<string, never>;
|
||||||
|
"/admin": Record<string, never>;
|
||||||
|
"/contacts": Record<string, never>;
|
||||||
|
"/dashboard": Record<string, never>;
|
||||||
|
"/deals": Record<string, never>;
|
||||||
|
"/forbidden": Record<string, never>;
|
||||||
|
"/login": Record<string, never>;
|
||||||
|
"/pricing": Record<string, never>;
|
||||||
|
"/signup": Record<string, never>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteNames {
|
||||||
|
"index": "/";
|
||||||
|
"admin": "/admin";
|
||||||
|
"contacts": "/contacts";
|
||||||
|
"dashboard": "/dashboard";
|
||||||
|
"deals": "/deals";
|
||||||
|
"forbidden": "/forbidden";
|
||||||
|
"login": "/login";
|
||||||
|
"pricing": "/pricing";
|
||||||
|
"signup": "/signup";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteQueries {
|
||||||
|
[path: string]: Record<string, string | number | boolean | null | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoutePath = keyof Routes;
|
||||||
|
export type RouteName = keyof RouteNames;
|
||||||
|
export type RouteQuery<P extends RoutePath> = P extends keyof RouteQueries
|
||||||
|
? RouteQueries[P]
|
||||||
|
: Record<string, string | number | boolean | null | undefined>;
|
||||||
|
type RouteValue = string | readonly string[] | undefined;
|
||||||
|
|
||||||
|
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
|
||||||
|
if (value === undefined) return "";
|
||||||
|
const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)];
|
||||||
|
return values.map((part) => encodeURIComponent(part)).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHref(path: string, params: Record<string, RouteValue> = {}): string {
|
||||||
|
const output: string[] = [];
|
||||||
|
for (const segment of path.split("/").filter(Boolean)) {
|
||||||
|
let name: string | undefined;
|
||||||
|
let optional = false;
|
||||||
|
let catchAll = false;
|
||||||
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
||||||
|
optional = true;
|
||||||
|
name = segment.slice(2, -2);
|
||||||
|
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
||||||
|
name = segment.slice(1, -1);
|
||||||
|
if (name.endsWith("?")) {
|
||||||
|
optional = true;
|
||||||
|
name = name.slice(0, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
output.push(segment);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (name.startsWith("...")) {
|
||||||
|
catchAll = true;
|
||||||
|
name = name.slice(3);
|
||||||
|
}
|
||||||
|
const value = params[name];
|
||||||
|
if (value === undefined && optional) continue;
|
||||||
|
if (value === undefined) throw new Error(`WRN-ROUTE-MISSING-PARAM: Missing route parameter '${name}'.`);
|
||||||
|
output.push(encodeRouteValue(value, catchAll));
|
||||||
|
}
|
||||||
|
return "/" + output.filter(Boolean).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function href<P extends RoutePath>(
|
||||||
|
path: P,
|
||||||
|
...args: keyof Routes[P] extends never
|
||||||
|
? []
|
||||||
|
: Record<string, never> extends Routes[P]
|
||||||
|
? [params?: Routes[P]]
|
||||||
|
: [params: Routes[P]]
|
||||||
|
): string {
|
||||||
|
const params = (args[0] ?? {}) as Record<string, RouteValue>;
|
||||||
|
return buildHref(String(path), params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function route<N extends RouteName>(
|
||||||
|
name: N,
|
||||||
|
...args: keyof Routes[RouteNames[N]] extends never
|
||||||
|
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||||
|
: Record<string, never> extends Routes[RouteNames[N]]
|
||||||
|
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||||
|
: [params: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||||
|
): string {
|
||||||
|
const paths: Record<RouteName, RoutePath> = {
|
||||||
|
"index": "/",
|
||||||
|
"admin": "/admin",
|
||||||
|
"contacts": "/contacts",
|
||||||
|
"dashboard": "/dashboard",
|
||||||
|
"deals": "/deals",
|
||||||
|
"forbidden": "/forbidden",
|
||||||
|
"login": "/login",
|
||||||
|
"pricing": "/pricing",
|
||||||
|
"signup": "/signup"
|
||||||
|
} as Record<RouteName, RoutePath>;
|
||||||
|
const output = buildHref(paths[name], (args[0] ?? {}) as Record<string, RouteValue>);
|
||||||
|
const query = args[1];
|
||||||
|
if (!query) return output;
|
||||||
|
const search = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(query))
|
||||||
|
if (value !== undefined && value !== null) search.set(key, String(value));
|
||||||
|
const text = search.toString();
|
||||||
|
return text ? `${output}?${text}` : output;
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "@iconify/tailwind4";
|
||||||
|
@source "../**/*.wrn";
|
||||||
|
@source "../../../packages/auth/components/*.wrn";
|
||||||
|
@source "../../../packages/ui/components/*.wrn";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
min-width: 320px;
|
||||||
|
background: var(--wrn-color-bg);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
color: var(--wrn-color-text);
|
||||||
|
background: var(--wrn-color-bg);
|
||||||
|
font-family: Inter, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-public {
|
||||||
|
min-height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
circle at 80% 10%,
|
||||||
|
color-mix(in srgb, var(--wrn-color-primary) 15%, transparent),
|
||||||
|
transparent 28%
|
||||||
|
),
|
||||||
|
var(--wrn-color-bg);
|
||||||
|
}
|
||||||
|
.crm-nav {
|
||||||
|
width: min(1180px, calc(100% - 2rem));
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.25rem 0;
|
||||||
|
}
|
||||||
|
.crm-brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
.crm-brand-mark {
|
||||||
|
display: grid;
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(135deg, var(--wrn-color-primary), #7c3aed);
|
||||||
|
box-shadow: 0 10px 24px color-mix(in srgb, var(--wrn-color-primary) 28%, transparent);
|
||||||
|
}
|
||||||
|
.crm-nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.crm-button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 1.1rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: white;
|
||||||
|
background: var(--wrn-color-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 10px 24px color-mix(in srgb, var(--wrn-color-primary) 22%, transparent);
|
||||||
|
}
|
||||||
|
.crm-button--ghost {
|
||||||
|
color: var(--wrn-color-text);
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.crm-hero {
|
||||||
|
width: min(1180px, calc(100% - 2rem));
|
||||||
|
margin: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.05fr 0.95fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rem;
|
||||||
|
padding: clamp(4rem, 9vw, 8rem) 0;
|
||||||
|
}
|
||||||
|
.crm-eyebrow {
|
||||||
|
color: var(--wrn-color-primary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.13em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.crm-hero h1 {
|
||||||
|
max-width: 12ch;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: clamp(3rem, 7vw, 5.5rem);
|
||||||
|
line-height: 0.98;
|
||||||
|
letter-spacing: -0.065em;
|
||||||
|
}
|
||||||
|
.crm-hero-copy {
|
||||||
|
max-width: 38rem;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
.crm-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.crm-preview {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
background: color-mix(in srgb, var(--wrn-color-surface) 88%, transparent);
|
||||||
|
box-shadow: 0 28px 80px rgba(15, 23, 42, 0.15);
|
||||||
|
transform: rotate(1.5deg);
|
||||||
|
}
|
||||||
|
.crm-preview-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem 0.25rem 1rem;
|
||||||
|
}
|
||||||
|
.crm-preview-bar span {
|
||||||
|
width: 0.6rem;
|
||||||
|
height: 0.6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--wrn-color-border);
|
||||||
|
}
|
||||||
|
.crm-preview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
.crm-preview-card {
|
||||||
|
min-height: 8rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--wrn-color-surface-2);
|
||||||
|
}
|
||||||
|
.crm-preview-card strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
.crm-preview-card small {
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-shell {
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 16rem 1fr;
|
||||||
|
background: var(--wrn-color-bg);
|
||||||
|
}
|
||||||
|
.crm-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.35rem;
|
||||||
|
border-right: 1px solid var(--wrn-color-border);
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
}
|
||||||
|
.crm-menu {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.crm-menu a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 0.85rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.crm-menu a:hover,
|
||||||
|
.crm-menu a[aria-current="page"] {
|
||||||
|
color: var(--wrn-color-primary);
|
||||||
|
background: color-mix(in srgb, var(--wrn-color-primary) 10%, transparent);
|
||||||
|
}
|
||||||
|
.crm-sidebar-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.crm-content {
|
||||||
|
min-width: 0;
|
||||||
|
padding: clamp(1.25rem, 4vw, 3rem);
|
||||||
|
}
|
||||||
|
.crm-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.crm-topbar h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.4rem);
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
.crm-topbar p {
|
||||||
|
margin: 0.4rem 0 0;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
}
|
||||||
|
.crm-panel {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
box-shadow: var(--wrn-shadow-1);
|
||||||
|
}
|
||||||
|
.crm-stat-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.crm-stat {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
}
|
||||||
|
.crm-stat span {
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.crm-stat strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
.crm-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.crm-list li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(10rem, 1fr) minmax(8rem, 0.7fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
background: var(--wrn-color-surface-2);
|
||||||
|
}
|
||||||
|
.crm-pill {
|
||||||
|
justify-self: end;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--wrn-color-primary);
|
||||||
|
background: color-mix(in srgb, var(--wrn-color-primary) 10%, transparent);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 750;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.crm-empty {
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
}
|
||||||
|
.crm-empty h2 {
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
color: var(--wrn-color-text);
|
||||||
|
}
|
||||||
|
.crm-status-page {
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at 50% 0%,
|
||||||
|
color-mix(in srgb, var(--wrn-color-primary) 12%, transparent),
|
||||||
|
transparent 45%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.crm-status-card {
|
||||||
|
width: min(34rem, 100%);
|
||||||
|
padding: clamp(1.5rem, 5vw, 3rem);
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
box-shadow: var(--wrn-shadow-2);
|
||||||
|
}
|
||||||
|
.crm-status-card h1 {
|
||||||
|
margin: 0.9rem 0 0.7rem;
|
||||||
|
font-size: clamp(1.8rem, 5vw, 2.5rem);
|
||||||
|
line-height: 1.08;
|
||||||
|
letter-spacing: -0.045em;
|
||||||
|
}
|
||||||
|
.crm-status-card > p {
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
.crm-status-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 1rem;
|
||||||
|
color: #b45309;
|
||||||
|
background: #fef3c7;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.crm-nav-links > a:not(.crm-button) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.crm-hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding-top: 3rem;
|
||||||
|
}
|
||||||
|
.crm-preview {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
.crm-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.crm-sidebar {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.crm-menu {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.crm-sidebar-footer {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.crm-stat-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.crm-hero h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
.crm-menu {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.crm-list li {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.crm-pill {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
|
||||||
|
//
|
||||||
|
// Type-only assertions for sectioned `api` blocks. Kept as a real .ts file (not
|
||||||
|
// wrnexus.generated.d.ts) because `skipLibCheck` exempts .d.ts contents from being
|
||||||
|
// checked; this file is compiled and checked normally by the project's own tsc.
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
|
||||||
|
declare namespace WRNexusGenerated {
|
||||||
|
type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer O>
|
||||||
|
? { input: I; output: O }
|
||||||
|
: T extends (...args: infer A) => infer R
|
||||||
|
? { input: A extends [any, infer I, ...any[]] ? I : unknown; output: Awaited<R> }
|
||||||
|
: { input: unknown; output: unknown };
|
||||||
|
type MiddlewareContext<T> = T extends (ctx: infer C, ...args: any[]) => any ? C : never;
|
||||||
|
type QueryContract<T> = T extends (db: any, args: infer A, ...rest: any[]) => infer R
|
||||||
|
? { args: A; result: Awaited<R> }
|
||||||
|
: T extends (db: any, ...rest: any[]) => infer R
|
||||||
|
? { args: Record<string, never>; result: Awaited<R> }
|
||||||
|
: never;
|
||||||
|
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
|
||||||
|
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
|
||||||
|
type RouteName = "admin" | "contacts" | "dashboard" | "deals" | "forbidden" | "index" | "login" | "pricing" | "signup";
|
||||||
|
type ApiRoute = "/api/contacts" | "/api/dashboard" | "/api/deals";
|
||||||
|
type RealtimeRoute = never;
|
||||||
|
type EnvironmentKey = never;
|
||||||
|
type TranslationKey = never;
|
||||||
|
type QueueName = never;
|
||||||
|
type CacheKey = never;
|
||||||
|
type Components = Record<string, never>;
|
||||||
|
interface ApiContracts {
|
||||||
|
"/api/dashboard": { GET: ApiContract<typeof import("../api/dashboard.ts")["GET"]> };
|
||||||
|
"/api/contacts": { GET: ApiContract<typeof import("../api/contacts.ts")["GET"]>; POST: ApiContract<typeof import("../api/contacts.ts")["POST"]> };
|
||||||
|
"/api/deals": { GET: ApiContract<typeof import("../api/deals.ts")["GET"]> };
|
||||||
|
}
|
||||||
|
interface MiddlewareContexts {
|
||||||
|
"authz": MiddlewareContext<(typeof import("../middleware/authz.ts"))["default"]>;
|
||||||
|
"protected": MiddlewareContext<(typeof import("../middleware/protected.ts"))["default"]>;
|
||||||
|
}
|
||||||
|
type DatabaseQueries = Record<string, never>;
|
||||||
|
type RealtimeMessages = Record<string, never>;
|
||||||
|
type QueuePayloads = Record<string, never>;
|
||||||
|
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
|
||||||
|
type AssertAssignable<Actual, Expected> = unknown extends Expected
|
||||||
|
? true
|
||||||
|
: [Actual] extends [Expected]
|
||||||
|
? [Exclude<keyof Actual, keyof Expected>] extends [never]
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false;
|
||||||
|
type __wrn_expect_true<T extends true> = T;
|
||||||
|
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
|
||||||
|
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// AUTO-GENERATED plugin type aggregation - do not edit.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "wrnexus-crm-example",
|
||||||
|
"version": "0.8.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run ../../packages/cli/src/index.ts dev .",
|
||||||
|
"build": "bun run ../../packages/cli/src/index.ts build .",
|
||||||
|
"db:migrate": "bun run ../../packages/cli/src/index.ts db migrate .",
|
||||||
|
"db:seed": "bun run ../../packages/cli/src/index.ts db seed .",
|
||||||
|
"test": "bun test test",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
|
"check": "bun run typecheck && bun run test && bun run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@wrnexus/auth": "workspace:*",
|
||||||
|
"@wrnexus/authz": "workspace:*",
|
||||||
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/styles": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.123",
|
||||||
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
|
"@types/bun": "^1.3.14",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { createDb, loadMigrations, migrate } from "@wrnexus/db";
|
||||||
|
import { sqlite } from "@wrnexus/db/sqlite";
|
||||||
|
import seed from "../app/db/seed.ts";
|
||||||
|
|
||||||
|
const root = join(import.meta.dir, "..");
|
||||||
|
const source = (path: string) => readFileSync(join(root, path), "utf8");
|
||||||
|
|
||||||
|
test("the CRM migration applies to SQLite, is idempotent, and the seed is repeatable", async () => {
|
||||||
|
const db = createDb(sqlite());
|
||||||
|
const migrations = join(root, "app", "db", "migrations");
|
||||||
|
|
||||||
|
expect(loadMigrations(migrations).map(({ name }) => name)).toEqual(["001_crm"]);
|
||||||
|
expect(await migrate(db, migrations)).toEqual(["001_crm"]);
|
||||||
|
expect(await migrate(db, migrations)).toEqual([]);
|
||||||
|
await seed(db);
|
||||||
|
await seed(db);
|
||||||
|
|
||||||
|
expect(await db.one<{ count: number }>("SELECT COUNT(*) count FROM crm_contacts")).toEqual({
|
||||||
|
count: 2,
|
||||||
|
});
|
||||||
|
expect(await db.one<{ count: number }>("SELECT COUNT(*) count FROM crm_deals")).toEqual({
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await db.all(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('_wrn_authz_assignment','_wrn_authz_grant') ORDER BY name",
|
||||||
|
),
|
||||||
|
).toHaveLength(2);
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authentication uses SQL, plugin migrations, signup/login components, and protected routes", () => {
|
||||||
|
expect(source("app/lib/auth.ts")).toContain("SqlAuthStore");
|
||||||
|
expect(source("wrnexus.config.ts")).toContain("migrations: true");
|
||||||
|
expect(source("app/pages/login.wrn")).toContain("<SignIn");
|
||||||
|
expect(source("app/pages/signup.wrn")).toContain("<SignUp");
|
||||||
|
expect(source("app/middleware/protected.ts")).toContain('requireAuth({ loginPath: "/login" })');
|
||||||
|
expect(source("app/pages/dashboard.wrn")).toContain("/api/auth/logout");
|
||||||
|
expect(source("app/pages/login.wrn")).toContain("AuthSplitLayout");
|
||||||
|
expect(source("app/pages/login.wrn")).toContain('data-slot="form"');
|
||||||
|
expect(source("app/pages/signup.wrn")).not.toContain("Already registered?");
|
||||||
|
expect(source("wrnexus.config.ts")).toContain('entry: "app/styles/global.css"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authorization is database-backed and guards both APIs and administration", () => {
|
||||||
|
expect(source("app/middleware/authz.ts")).toContain("dbPermissionStore(getDb())");
|
||||||
|
expect(source("app/lib/auth.ts")).toContain('assignRole(user.id, "sales-rep")');
|
||||||
|
expect(source("app/api/contacts.ts")).toContain('can(ctx, "contact:write")');
|
||||||
|
expect(source("app/api/deals.ts")).toContain('can(ctx, "deal:read")');
|
||||||
|
expect(source("app/middleware/protected.ts")).toContain('can(ctx, "admin:access")');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the app includes public, authentication, and protected CRM pages", () => {
|
||||||
|
for (const page of [
|
||||||
|
"index",
|
||||||
|
"pricing",
|
||||||
|
"login",
|
||||||
|
"signup",
|
||||||
|
"dashboard",
|
||||||
|
"contacts",
|
||||||
|
"deals",
|
||||||
|
"admin",
|
||||||
|
"forbidden",
|
||||||
|
]) {
|
||||||
|
expect(source(`app/pages/${page}.wrn`)).toContain("page ");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("protected CRM data is loaded on the server without browser API bindings", () => {
|
||||||
|
for (const page of ["contacts", "deals", "dashboard"]) {
|
||||||
|
const contents = source(`app/pages/${page}.wrn`);
|
||||||
|
expect(contents).toContain("load server");
|
||||||
|
expect(contents).not.toContain(' api="');
|
||||||
|
}
|
||||||
|
expect(source("app/api/dashboard.ts")).toContain("pipeline_value_cents");
|
||||||
|
expect(source("app/lib/workspace.ts")).toContain("INSERT OR IGNORE INTO crm_contacts");
|
||||||
|
expect(source("app/lib/auth.ts")).toContain("ensureCrmWorkspace(user.id)");
|
||||||
|
expect(source("app/middleware/protected.ts")).toContain('new URL("/forbidden", ctx.url)');
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": { "noEmit": true },
|
||||||
|
"include": ["app/**/*.ts", "test/**/*.ts", "wrnexus.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import type { AuthConfig } from "@wrnexus/auth";
|
||||||
|
import type { AppConfig } from "@wrnexus/styles";
|
||||||
|
import { auth } from "./app/lib/auth.ts";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
seo: {
|
||||||
|
title: "Northstar CRM",
|
||||||
|
titleTemplate: "%s | Northstar CRM",
|
||||||
|
description: "A complete WrNexus SQLite CRM example.",
|
||||||
|
canonicalBase: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
theme: { palette: "blue", default: "light" },
|
||||||
|
db: { driver: "sqlite", url: "file:./crm.sqlite" },
|
||||||
|
styles: {
|
||||||
|
entry: "app/styles/global.css",
|
||||||
|
async process({ entryPath, appRoot, mode }) {
|
||||||
|
const args = ["@tailwindcss/cli", "-i", entryPath!];
|
||||||
|
if (mode === "production") args.push("--minify");
|
||||||
|
return await Bun.$.cwd(appRoot)`bunx ${args}`.text();
|
||||||
|
},
|
||||||
|
failureMode: "throw",
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
engine: auth,
|
||||||
|
routes: true,
|
||||||
|
middleware: true,
|
||||||
|
components: true,
|
||||||
|
migrations: true,
|
||||||
|
baseUrl: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
profiles: {
|
||||||
|
test: { db: { driver: "sqlite", url: `file:${join(import.meta.dir, ".tmp-test.sqlite")}` } },
|
||||||
|
},
|
||||||
|
security: { cors: { enabled: false } },
|
||||||
|
} satisfies AppConfig & { auth: AuthConfig };
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -4,8 +4,6 @@ import { join } from "node:path";
|
|||||||
import type { AppConfig } from "@wrnexus/styles";
|
import type { AppConfig } from "@wrnexus/styles";
|
||||||
|
|
||||||
const config: AppConfig = {
|
const config: AppConfig = {
|
||||||
compatibilityDate: "2026-08-02",
|
|
||||||
frameworkBehaviour: 1,
|
|
||||||
// v0.8 defaults: explicit imports, strict template types, safe stores, and
|
// v0.8 defaults: explicit imports, strict template types, safe stores, and
|
||||||
// automatic progressive navigation. Package plugins are discovered from the
|
// automatic progressive navigation. Package plugins are discovered from the
|
||||||
// installed packages above; add custom plugins to this array when needed.
|
// installed packages above; add custom plugins to this array when needed.
|
||||||
@@ -19,14 +17,7 @@ const config: AppConfig = {
|
|||||||
checkComponentProps: true,
|
checkComponentProps: true,
|
||||||
generateDeclarations: true,
|
generateDeclarations: true,
|
||||||
},
|
},
|
||||||
functions: { legacyDefaultRuntime: "current" },
|
|
||||||
stores: { strictMutations: true, persistence: true },
|
stores: { strictMutations: true, persistence: true },
|
||||||
compatibility: {
|
|
||||||
legacyEmit: false,
|
|
||||||
legacyEventProps: false,
|
|
||||||
legacyComponentDiscovery: false,
|
|
||||||
stringLayouts: false,
|
|
||||||
},
|
|
||||||
experimental: {},
|
experimental: {},
|
||||||
|
|
||||||
performance: {
|
performance: {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { join } from "node:path";
|
|||||||
import type { AppConfig } from "@wrnexus/styles";
|
import type { AppConfig } from "@wrnexus/styles";
|
||||||
|
|
||||||
const config: AppConfig = {
|
const config: AppConfig = {
|
||||||
compatibilityDate: "2026-08-02",
|
|
||||||
frameworkBehaviour: 1,
|
|
||||||
// v0.8 defaults: explicit imports, strict template types, safe stores, and
|
// v0.8 defaults: explicit imports, strict template types, safe stores, and
|
||||||
// automatic progressive navigation. Package plugins are discovered from the
|
// automatic progressive navigation. Package plugins are discovered from the
|
||||||
// installed packages above; add custom plugins to this array when needed.
|
// installed packages above; add custom plugins to this array when needed.
|
||||||
@@ -19,14 +17,7 @@ const config: AppConfig = {
|
|||||||
checkComponentProps: true,
|
checkComponentProps: true,
|
||||||
generateDeclarations: true,
|
generateDeclarations: true,
|
||||||
},
|
},
|
||||||
functions: { legacyDefaultRuntime: "current" },
|
|
||||||
stores: { strictMutations: true, persistence: true },
|
stores: { strictMutations: true, persistence: true },
|
||||||
compatibility: {
|
|
||||||
legacyEmit: false,
|
|
||||||
legacyEventProps: false,
|
|
||||||
legacyComponentDiscovery: false,
|
|
||||||
stringLayouts: false,
|
|
||||||
},
|
|
||||||
experimental: {},
|
experimental: {},
|
||||||
|
|
||||||
performance: {
|
performance: {
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@
|
|||||||
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
|
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
|
||||||
"sbom": "node scripts/generate-sbom.mjs",
|
"sbom": "node scripts/generate-sbom.mjs",
|
||||||
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write",
|
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write",
|
||||||
"check:production": "bun run check:workspace && bun run check:generated-types && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples",
|
"check:production": "bun run check:workspace && bun run check:generated-types && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples && bun run test:services && bun run test:editor",
|
||||||
"validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
|
"validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
|
||||||
"stage:packages": "bun run scripts/publish-packages.ts",
|
"stage:packages": "bun run scripts/publish-packages.ts",
|
||||||
"test:staged-consumers": "node scripts/test-staged-consumers.mjs",
|
"test:staged-consumers": "node scripts/test-staged-consumers.mjs",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.4.0",
|
||||||
"@types/react": "^19.2.18",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.4",
|
"@types/react-dom": "^19.2.4",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
|
|||||||
@@ -25,17 +25,17 @@ component SignIn {
|
|||||||
class = ""
|
class = ""
|
||||||
}
|
}
|
||||||
view {
|
view {
|
||||||
<section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
<section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='wrn-auth-sign-in w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||||
<header class="mb-6 space-y-1.5">
|
<header class="wrn-auth-sign-in__header mb-6 space-y-1.5">
|
||||||
<h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1>
|
<h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1>
|
||||||
<p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
<p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||||
</header>
|
</header>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="wrn-auth-sign-in__form">
|
||||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||||
<input type="hidden" name="deviceFingerprint" value="" />
|
<input type="hidden" name="deviceFingerprint" value="" />
|
||||||
<input type="hidden" name="deviceName" value="" />
|
<input type="hidden" name="deviceName" value="" />
|
||||||
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||||
<div class="space-y-1.5">
|
<div>
|
||||||
<TogglePassword
|
<TogglePassword
|
||||||
label="{passwordLabel}"
|
label="{passwordLabel}"
|
||||||
cornerHint="Forgot password?"
|
cornerHint="Forgot password?"
|
||||||
@@ -48,9 +48,7 @@ component SignIn {
|
|||||||
size="{size}"
|
size="{size}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if showRemember}
|
{#if showRemember}<div class="wrn-auth-sign-in__remember"><Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" /></div>{/if}
|
||||||
<Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" />
|
|
||||||
{/if}
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
@@ -64,4 +62,18 @@ component SignIn {
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
style {
|
||||||
|
.wrn-auth-sign-in__header { margin-bottom: 1.5rem; }
|
||||||
|
.wrn-auth-sign-in__header h1 { font-size: 1.75rem; line-height: 1.15; }
|
||||||
|
.wrn-auth-sign-in__header p { margin-top: 0.35rem; font-size: 0.9375rem; line-height: 1.5; }
|
||||||
|
.wrn-auth-sign-in__form { display: grid; gap: 1.15rem; }
|
||||||
|
.wrn-auth-sign-in__form > * { margin: 0 !important; }
|
||||||
|
.wrn-auth-sign-in__remember { margin-top: -0.25rem !important; }
|
||||||
|
.wrn-auth-sign-in__form .wrn-next__field-heading label,
|
||||||
|
.wrn-auth-sign-in__form .wrn-next__password-heading { font-size: 0.875rem; font-weight: 650; }
|
||||||
|
.wrn-auth-sign-in__form .wrn-next__field-control > input { min-height: 3rem; font-size: 0.875rem; }
|
||||||
|
.wrn-auth-sign-in__form [data-error]:empty { display: none; }
|
||||||
|
.wrn-auth-sign-in__form .wrn-action[data-full-width="true"] { margin-top: 0.15rem !important; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ component SignUp {
|
|||||||
<section
|
<section
|
||||||
{...attrs}
|
{...attrs}
|
||||||
data-wrnexus-runtime="auth"
|
data-wrnexus-runtime="auth"
|
||||||
class='w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
|
class='wrn-auth-sign-up w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="mb-6 space-y-1.5"
|
class="mb-6 space-y-1.5"
|
||||||
@@ -50,7 +50,7 @@ component SignUp {
|
|||||||
data-schema='{schema}'
|
data-schema='{schema}'
|
||||||
data-redirect='{redirect}'
|
data-redirect='{redirect}'
|
||||||
novalidate
|
novalidate
|
||||||
class="grid gap-4 sm:grid-cols-2"
|
class="wrn-auth-sign-up__form"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
id="auth-sign-up-name"
|
id="auth-sign-up-name"
|
||||||
@@ -61,7 +61,6 @@ component SignUp {
|
|||||||
icon="icon-[lucide--user]"
|
icon="icon-[lucide--user]"
|
||||||
color="{color}"
|
color="{color}"
|
||||||
size="{size}"
|
size="{size}"
|
||||||
class="sm:col-span-2"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -100,7 +99,6 @@ component SignUp {
|
|||||||
icon="icon-[lucide--circle-user-round]"
|
icon="icon-[lucide--circle-user-round]"
|
||||||
color="{color}"
|
color="{color}"
|
||||||
size="{size}"
|
size="{size}"
|
||||||
class="sm:col-span-2"
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -176,15 +174,7 @@ component SignUp {
|
|||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button
|
<div class="wrn-auth-sign-up__submit"><Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" /></div>
|
||||||
type="submit"
|
|
||||||
label="{submitLabel}"
|
|
||||||
variant="solid"
|
|
||||||
color="{color}"
|
|
||||||
size="{size}"
|
|
||||||
fullWidth="true"
|
|
||||||
class="wrn-auth-sign-up__wide sm:col-span-2"
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
@@ -196,9 +186,19 @@ component SignUp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
style {
|
style {
|
||||||
|
.wrn-auth-sign-up > header { margin-bottom: 1.5rem; }
|
||||||
|
.wrn-auth-sign-up > header h1 { font-size: 1.75rem; line-height: 1.15; letter-spacing: -0.025em; }
|
||||||
|
.wrn-auth-sign-up > header p { margin-top: 0.35rem; font-size: 0.9375rem; line-height: 1.5; }
|
||||||
|
.wrn-auth-sign-up__form { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.15rem; }
|
||||||
|
.wrn-auth-sign-up__form .wrn-next__field-heading label,
|
||||||
|
.wrn-auth-sign-up__form .wrn-next--strong-password > label { font-size: 0.875rem; font-weight: 650; }
|
||||||
|
.wrn-auth-sign-up__form .wrn-next__field-control > input { min-height: 3rem; font-size: 0.875rem; }
|
||||||
|
.wrn-auth-sign-up__form [data-error]:empty { display: none; }
|
||||||
.wrn-auth-sign-up__wide {
|
.wrn-auth-sign-up__wide {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.wrn-auth-sign-up__submit { width: 100%; margin-top: 0.25rem; }
|
||||||
|
.wrn-auth-sign-up__submit .wrn-btn { min-height: 3rem; font-size: 0.9375rem; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.12",
|
"version": "0.8.18",
|
||||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
+34
-10
@@ -11,6 +11,7 @@ import {
|
|||||||
import { inferIdentityType, normalizeIdentity, publicUser } from "./normalize.ts";
|
import { inferIdentityType, normalizeIdentity, publicUser } from "./normalize.ts";
|
||||||
import { assertPasskeyProvider, MemoryPasskeyChallengeStore } from "./passkeys/index.ts";
|
import { assertPasskeyProvider, MemoryPasskeyChallengeStore } from "./passkeys/index.ts";
|
||||||
import { evaluateAuthRisk } from "./risk.ts";
|
import { evaluateAuthRisk } from "./risk.ts";
|
||||||
|
import { setDefaultAuthEngine } from "./runtime.ts";
|
||||||
import type { AuthStore } from "./store.ts";
|
import type { AuthStore } from "./store.ts";
|
||||||
import { generateTotpSecret, totpUri, verifyTotp } from "./totp/index.ts";
|
import { generateTotpSecret, totpUri, verifyTotp } from "./totp/index.ts";
|
||||||
import type {
|
import type {
|
||||||
@@ -160,10 +161,6 @@ export interface AuthEngine {
|
|||||||
key: string;
|
key: string;
|
||||||
response: unknown;
|
response: unknown;
|
||||||
name?: string;
|
name?: string;
|
||||||
/** @deprecated Verification uses the RP ID bound to the issued challenge. */
|
|
||||||
rpId?: string;
|
|
||||||
/** @deprecated Verification uses the origin bound to the issued challenge. */
|
|
||||||
origin?: string;
|
|
||||||
},
|
},
|
||||||
): Promise<boolean>;
|
): Promise<boolean>;
|
||||||
beginPasskeyAuthentication(input: {
|
beginPasskeyAuthentication(input: {
|
||||||
@@ -176,10 +173,6 @@ export interface AuthEngine {
|
|||||||
response: unknown;
|
response: unknown;
|
||||||
/** Request metadata used only for the resulting session. */
|
/** Request metadata used only for the resulting session. */
|
||||||
session?: Partial<AuthSession>;
|
session?: Partial<AuthSession>;
|
||||||
/** @deprecated Verification uses the RP ID bound to the issued challenge. */
|
|
||||||
rpId?: string;
|
|
||||||
/** @deprecated Verification uses the origin bound to the issued challenge. */
|
|
||||||
origin?: string;
|
|
||||||
}): Promise<AuthResult>;
|
}): Promise<AuthResult>;
|
||||||
changePassword(
|
changePassword(
|
||||||
userId: string,
|
userId: string,
|
||||||
@@ -274,7 +267,29 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
|||||||
if (options.secret.length < MIN_SECRET_LENGTH) {
|
if (options.secret.length < MIN_SECRET_LENGTH) {
|
||||||
throw new TypeError(`auth secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
throw new TypeError(`auth secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
||||||
}
|
}
|
||||||
const store = options.store;
|
let resolvedStore: AuthStore | undefined;
|
||||||
|
const resolveStore = (): AuthStore => {
|
||||||
|
if (resolvedStore) return resolvedStore;
|
||||||
|
resolvedStore = typeof options.store === "function" ? options.store() : options.store;
|
||||||
|
if (!resolvedStore || typeof resolvedStore !== "object") {
|
||||||
|
throw new TypeError("WRN-AUTH-STORE: the auth store factory did not return an AuthStore");
|
||||||
|
}
|
||||||
|
return resolvedStore;
|
||||||
|
};
|
||||||
|
// AuthEngine.store remains source-compatible while deferring the factory
|
||||||
|
// until the first actual property read or method call.
|
||||||
|
const store = new Proxy({} as AuthStore, {
|
||||||
|
get(_target, property) {
|
||||||
|
const value = Reflect.get(resolveStore() as object, property);
|
||||||
|
return typeof value === "function" ? value.bind(resolveStore()) : value;
|
||||||
|
},
|
||||||
|
set(_target, property, value) {
|
||||||
|
return Reflect.set(resolveStore() as object, property, value);
|
||||||
|
},
|
||||||
|
has(_target, property) {
|
||||||
|
return Reflect.has(resolveStore() as object, property);
|
||||||
|
},
|
||||||
|
});
|
||||||
const now = () => {
|
const now = () => {
|
||||||
const value = options.clock?.now() ?? Date.now();
|
const value = options.clock?.now() ?? Date.now();
|
||||||
if (!Number.isFinite(value)) throw new Error("WRN-AUTH-CLOCK: clock returned an invalid time");
|
if (!Number.isFinite(value)) throw new Error("WRN-AUTH-CLOCK: clock returned an invalid time");
|
||||||
@@ -832,7 +847,7 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
|||||||
store,
|
store,
|
||||||
onSignedIn: options.onSignedIn,
|
onSignedIn: options.onSignedIn,
|
||||||
onSignedOut: options.onSignedOut,
|
onSignedOut: options.onSignedOut,
|
||||||
onSuccessfulSignUp: options.onSuccessfulSignUp ?? options.onSuccessfullSignUp,
|
onSuccessfulSignUp: options.onSuccessfulSignUp,
|
||||||
|
|
||||||
async register(input) {
|
async register(input) {
|
||||||
try {
|
try {
|
||||||
@@ -1657,6 +1672,11 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
|||||||
(typeof metadata.deviceId === "string" ? metadata.deviceId : undefined),
|
(typeof metadata.deviceId === "string" ? metadata.deviceId : undefined),
|
||||||
trusted: Boolean(rememberedDevice) || input.session?.trusted === true,
|
trusted: Boolean(rememberedDevice) || input.session?.trusted === true,
|
||||||
fingerprint: typeof metadata.fingerprint === "string" ? metadata.fingerprint : undefined,
|
fingerprint: typeof metadata.fingerprint === "string" ? metadata.fingerprint : undefined,
|
||||||
|
metadata: {
|
||||||
|
...(input.session?.metadata ?? {}),
|
||||||
|
mfaVerifiedAt: now(),
|
||||||
|
mfaMethod: input.method,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
user.lastLoginAt = now();
|
user.lastLoginAt = now();
|
||||||
user.updatedAt = now();
|
user.updatedAt = now();
|
||||||
@@ -2284,5 +2304,9 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The application config is evaluated in both dev and generated production
|
||||||
|
// runtimes. Registering here makes that configured engine available to the
|
||||||
|
// package-owned routes without an application startup shim.
|
||||||
|
setDefaultAuthEngine(engine);
|
||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import {
|
|||||||
getAuthUser,
|
getAuthUser,
|
||||||
} from "../middleware.ts";
|
} from "../middleware.ts";
|
||||||
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
|
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
|
||||||
import type {
|
import type { AuthSessionVerificationHandler } from "../types.ts";
|
||||||
AuthSessionVerificationHandler,
|
import { completeAuth, startAuth, type OAuthProvider } from "@wrnexus/oauth";
|
||||||
AuthSignedInHandler,
|
import { assignDefaultAuthzRoles } from "@wrnexus/authz/defaults";
|
||||||
AuthSignedOutHandler,
|
|
||||||
} from "../types.ts";
|
|
||||||
|
|
||||||
function text(value: unknown): string {
|
function text(value: unknown): string {
|
||||||
return typeof value === "string" ? value : value == null ? "" : String(value);
|
return typeof value === "string" ? value : value == null ? "" : String(value);
|
||||||
@@ -53,20 +51,29 @@ export interface AuthHttpOptions {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
schemas?: AuthSchemaOverrides | AuthSchemaSet;
|
schemas?: AuthSchemaOverrides | AuthSchemaSet;
|
||||||
passkey?: AuthPasskeyHttpOptions;
|
passkey?: AuthPasskeyHttpOptions;
|
||||||
/** @deprecated Prefer createAuthEngine({ onSignedIn }). */
|
|
||||||
onSignedIn?: AuthSignedInHandler;
|
|
||||||
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
|
|
||||||
onSignedOut?: AuthSignedOutHandler;
|
|
||||||
onSessionVerification?: AuthSessionVerificationHandler;
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
|
oauth?: Record<string, OAuthProvider>;
|
||||||
|
oauthMfaPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
||||||
const engine = options.engine;
|
const engine = options.engine;
|
||||||
const schemas = resolveAuthSchemas(options.schemas);
|
const schemas = resolveAuthSchemas(options.schemas);
|
||||||
const onSignedIn = options.onSignedIn ?? engine.onSignedIn;
|
const onSignedIn = engine.onSignedIn;
|
||||||
const onSignedOut = options.onSignedOut ?? engine.onSignedOut;
|
const onSignedOut = engine.onSignedOut;
|
||||||
const onSuccessfulSignUp = engine.onSuccessfulSignUp;
|
const onSuccessfulSignUp = engine.onSuccessfulSignUp;
|
||||||
|
|
||||||
|
function oauthProvider(ctx: Context): OAuthProvider | undefined {
|
||||||
|
return options.oauth?.[String(ctx.params.provider ?? "").toLowerCase()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function oauthRedirectUri(ctx: Context, provider: OAuthProvider): string {
|
||||||
|
return new URL(
|
||||||
|
`/api/auth/oauth/${encodeURIComponent(provider.name)}/callback`,
|
||||||
|
options.baseUrl ?? ctx.url.origin,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
function signupRedirect(ctx: Context, value: string | undefined, fallback: string): Response {
|
function signupRedirect(ctx: Context, value: string | undefined, fallback: string): Response {
|
||||||
const path = safeAuthReturnTo(value, ctx.url.origin) ?? fallback;
|
const path = safeAuthReturnTo(value, ctx.url.origin) ?? fallback;
|
||||||
return Response.redirect(new URL(path, ctx.url), 303);
|
return Response.redirect(new URL(path, ctx.url), 303);
|
||||||
@@ -88,6 +95,71 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
async oauthProviders(ctx: Context): Promise<Response> {
|
||||||
|
return json({
|
||||||
|
ok: true,
|
||||||
|
providers: Object.values(options.oauth ?? {}).map((provider) => ({
|
||||||
|
id: provider.name,
|
||||||
|
name: provider.name,
|
||||||
|
label: `Continue with ${provider.name.charAt(0).toUpperCase()}${provider.name.slice(1)}`,
|
||||||
|
href: `/api/auth/oauth/${encodeURIComponent(provider.name)}?returnTo=${encodeURIComponent(
|
||||||
|
safeAuthReturnTo(ctx.url.searchParams.get("returnTo") ?? undefined, ctx.url.origin) ??
|
||||||
|
"/",
|
||||||
|
)}`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async startOAuth(ctx: Context): Promise<Response> {
|
||||||
|
const provider = oauthProvider(ctx);
|
||||||
|
if (!provider) return json({ ok: false, error: "OAuth provider not configured" }, 404);
|
||||||
|
const returnTo =
|
||||||
|
safeAuthReturnTo(ctx.url.searchParams.get("returnTo") ?? undefined, ctx.url.origin) ?? "/";
|
||||||
|
const started = await startAuth(provider, { redirectUri: oauthRedirectUri(ctx, provider) });
|
||||||
|
ctx.cookies.transaction(`wrnexus.oauth.${provider.name}`).set({
|
||||||
|
state: started.state,
|
||||||
|
verifier: started.verifier,
|
||||||
|
returnTo,
|
||||||
|
});
|
||||||
|
return Response.redirect(started.url, 302);
|
||||||
|
},
|
||||||
|
|
||||||
|
async completeOAuth(ctx: Context): Promise<Response> {
|
||||||
|
const provider = oauthProvider(ctx);
|
||||||
|
if (!provider) return json({ ok: false, error: "OAuth provider not configured" }, 404);
|
||||||
|
const transaction = ctx.cookies
|
||||||
|
.transaction<{
|
||||||
|
state: string;
|
||||||
|
verifier: string;
|
||||||
|
returnTo: string;
|
||||||
|
}>(`wrnexus.oauth.${provider.name}`)
|
||||||
|
.consume();
|
||||||
|
const state = ctx.url.searchParams.get("state");
|
||||||
|
const code = ctx.url.searchParams.get("code");
|
||||||
|
if (!transaction || !state || transaction.state !== state || !code) {
|
||||||
|
return json({ ok: false, error: "OAuth transaction is invalid or expired" }, 400);
|
||||||
|
}
|
||||||
|
const completed = await completeAuth(provider, {
|
||||||
|
code,
|
||||||
|
verifier: transaction.verifier,
|
||||||
|
redirectUri: oauthRedirectUri(ctx, provider),
|
||||||
|
});
|
||||||
|
const result = await engine.loginWithOAuth(
|
||||||
|
provider.name,
|
||||||
|
completed.profile,
|
||||||
|
completed.tokens,
|
||||||
|
);
|
||||||
|
if (result.code === "mfa-required" && result.mfaToken) {
|
||||||
|
ctx.cookies.transaction("wrnexus.auth.mfa", { sameSite: "Lax", maxAge: 300 }).set({
|
||||||
|
mfaToken: result.mfaToken,
|
||||||
|
returnTo: transaction.returnTo,
|
||||||
|
});
|
||||||
|
return Response.redirect(new URL(options.oauthMfaPath ?? "/two-factor", ctx.url), 303);
|
||||||
|
}
|
||||||
|
if (!result.ok || !result.session || !result.user) return json(result, 401);
|
||||||
|
establishAuthSession(ctx, result.session, result.user);
|
||||||
|
if (onSignedIn) return onSignedIn(ctx, transaction.returnTo);
|
||||||
|
return Response.redirect(new URL(transaction.returnTo, ctx.url), 303);
|
||||||
|
},
|
||||||
async verifySession(ctx: Context): Promise<Response> {
|
async verifySession(ctx: Context): Promise<Response> {
|
||||||
const user = getAuthUser(ctx);
|
const user = getAuthUser(ctx);
|
||||||
if (options.onSessionVerification) {
|
if (options.onSessionVerification) {
|
||||||
@@ -118,6 +190,8 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
});
|
});
|
||||||
if (!result.ok || !result.user) return json(result, 400);
|
if (!result.ok || !result.user) return json(result, 400);
|
||||||
|
|
||||||
|
await assignDefaultAuthzRoles(result.user.id, "signup");
|
||||||
|
|
||||||
const action = await onSuccessfulSignUp?.(ctx, result.user);
|
const action = await onSuccessfulSignUp?.(ctx, result.user);
|
||||||
if (action instanceof Response) return action;
|
if (action instanceof Response) return action;
|
||||||
if (action?.autoSignIn) {
|
if (action?.autoSignIn) {
|
||||||
@@ -238,6 +312,7 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
password: text(input.password) || undefined,
|
password: text(input.password) || undefined,
|
||||||
displayName: text(input.displayName) || undefined,
|
displayName: text(input.displayName) || undefined,
|
||||||
});
|
});
|
||||||
|
if (result.ok && result.user) await assignDefaultAuthzRoles(result.user.id, "invitation");
|
||||||
return json(result, result.ok ? 200 : 400);
|
return json(result, result.ok ? 200 : 400);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -411,12 +486,18 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
const validation = await parseBody(schemas.mfaComplete, ctx.req);
|
const validation = await parseBody(schemas.mfaComplete, ctx.req);
|
||||||
if (!validation.ok) return validation.response;
|
if (!validation.ok) return validation.response;
|
||||||
const input = validation.value;
|
const input = validation.value;
|
||||||
|
const continuation = ctx.cookies
|
||||||
|
.transaction<{ mfaToken: string; returnTo?: string }>("wrnexus.auth.mfa", {
|
||||||
|
sameSite: "Lax",
|
||||||
|
maxAge: 300,
|
||||||
|
})
|
||||||
|
.consume();
|
||||||
const methodValue = text(input.method);
|
const methodValue = text(input.method);
|
||||||
const method = ["totp", "recovery-code", "email-otp", "sms-otp"].includes(methodValue)
|
const method = ["totp", "recovery-code", "email-otp", "sms-otp"].includes(methodValue)
|
||||||
? (methodValue as "totp" | "recovery-code" | "email-otp" | "sms-otp")
|
? (methodValue as "totp" | "recovery-code" | "email-otp" | "sms-otp")
|
||||||
: "totp";
|
: "totp";
|
||||||
const result = await engine.completeMfa({
|
const result = await engine.completeMfa({
|
||||||
mfaToken: text(input.mfaToken),
|
mfaToken: text(input.mfaToken) || continuation?.mfaToken || "",
|
||||||
method,
|
method,
|
||||||
code: text(input.code),
|
code: text(input.code),
|
||||||
challengeId: text(input.challengeId) || undefined,
|
challengeId: text(input.challengeId) || undefined,
|
||||||
@@ -429,7 +510,10 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
if (!result.ok || !result.session || !result.user) return json(result, 400);
|
if (!result.ok || !result.session || !result.user) return json(result, 400);
|
||||||
establishAuthSession(ctx, result.session, result.user);
|
establishAuthSession(ctx, result.session, result.user);
|
||||||
if (onSignedIn) {
|
if (onSignedIn) {
|
||||||
return onSignedIn(ctx, safeAuthReturnTo(text(input.returnTo) || undefined, ctx.url.origin));
|
return onSignedIn(
|
||||||
|
ctx,
|
||||||
|
safeAuthReturnTo(text(input.returnTo) || continuation?.returnTo, ctx.url.origin),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return json(result);
|
return json(result);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export {
|
|||||||
clearAuthSession,
|
clearAuthSession,
|
||||||
getAuthUser,
|
getAuthUser,
|
||||||
getAuthSession,
|
getAuthSession,
|
||||||
|
requireAuthUser,
|
||||||
|
AuthRequiredError,
|
||||||
isAuthenticatedContext,
|
isAuthenticatedContext,
|
||||||
AUTH_SESSION_KEY,
|
AUTH_SESSION_KEY,
|
||||||
} from "./middleware.ts";
|
} from "./middleware.ts";
|
||||||
@@ -20,6 +22,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
authPlugin,
|
authPlugin,
|
||||||
authComponentsDir,
|
authComponentsDir,
|
||||||
|
validateProductionAuthConfig,
|
||||||
type AuthConfig,
|
type AuthConfig,
|
||||||
type AuthRoutesConfig,
|
type AuthRoutesConfig,
|
||||||
type AuthPluginOptions,
|
type AuthPluginOptions,
|
||||||
|
|||||||
@@ -79,6 +79,23 @@ export function getAuthUser(ctx: Context): AuthPublicUser | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Return a typed authenticated user or fail closed for direct handler use. */
|
||||||
|
export function requireAuthUser(ctx: Context): AuthPublicUser {
|
||||||
|
const user = getAuthUser(ctx);
|
||||||
|
if (!user) throw new AuthRequiredError();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthRequiredError extends Error {
|
||||||
|
readonly code = "WRN-AUTH-REQUIRED";
|
||||||
|
readonly status = 401;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("Authentication is required");
|
||||||
|
this.name = "AuthRequiredError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getAuthSession(ctx: Context): AuthSession | null {
|
export function getAuthSession(ctx: Context): AuthSession | null {
|
||||||
return (ctx.locals.authSession as AuthSession | undefined) ?? null;
|
return (ctx.locals.authSession as AuthSession | undefined) ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-17
@@ -2,13 +2,16 @@ import { readFileSync } from "node:fs";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
||||||
|
import {
|
||||||
|
discord,
|
||||||
|
github,
|
||||||
|
google,
|
||||||
|
type OAuthProvider,
|
||||||
|
type ProviderCredentials,
|
||||||
|
} from "@wrnexus/oauth";
|
||||||
import type { AuthEngine } from "./engine.ts";
|
import type { AuthEngine } from "./engine.ts";
|
||||||
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
||||||
import type {
|
import type { AuthSessionVerificationHandler } from "./types.ts";
|
||||||
AuthSessionVerificationHandler,
|
|
||||||
AuthSignedInHandler,
|
|
||||||
AuthSignedOutHandler,
|
|
||||||
} from "./types.ts";
|
|
||||||
import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts";
|
import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts";
|
||||||
import {
|
import {
|
||||||
clearDefaultAuthEngine,
|
clearDefaultAuthEngine,
|
||||||
@@ -36,8 +39,11 @@ export interface AuthRoutesConfig {
|
|||||||
sessions?: boolean;
|
sessions?: boolean;
|
||||||
impersonation?: boolean;
|
impersonation?: boolean;
|
||||||
passkeys?: boolean;
|
passkeys?: boolean;
|
||||||
|
oauth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthOAuthProviderConfig = OAuthProvider | ProviderCredentials;
|
||||||
|
|
||||||
export interface AuthConfig {
|
export interface AuthConfig {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
engine?: AuthEngine;
|
engine?: AuthEngine;
|
||||||
@@ -52,16 +58,36 @@ export interface AuthConfig {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
csrf?: boolean;
|
csrf?: boolean;
|
||||||
passkey?: AuthPasskeyHttpOptions;
|
passkey?: AuthPasskeyHttpOptions;
|
||||||
/** @deprecated Prefer createAuthEngine({ onSignedIn }). */
|
oauth?: Partial<Record<"google" | "github" | "discord", AuthOAuthProviderConfig>> &
|
||||||
onSignedIn?: AuthSignedInHandler;
|
Record<string, AuthOAuthProviderConfig>;
|
||||||
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
|
oauthMfaPath?: string;
|
||||||
onSignedOut?: AuthSignedOutHandler;
|
/** Disable only when an external deployment gate performs equivalent checks. */
|
||||||
|
productionValidation?: boolean;
|
||||||
/** Shared SSO cookie whose value is an AuthEngine session id. */
|
/** Shared SSO cookie whose value is an AuthEngine session id. */
|
||||||
sessionCookieName?: string;
|
sessionCookieName?: string;
|
||||||
/** Customize the package forward-auth verification response. */
|
/** Customize the package forward-auth verification response. */
|
||||||
onSessionVerification?: AuthSessionVerificationHandler;
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateProductionAuthConfig(
|
||||||
|
config: Pick<AuthConfig, "baseUrl" | "oauth">,
|
||||||
|
env: Record<string, string | undefined> = process.env,
|
||||||
|
): void {
|
||||||
|
const secret = env.AUTH_SECRET?.trim() ?? "";
|
||||||
|
if (secret.length < 32 || /^(?:change-me|development|test-only)/i.test(secret)) {
|
||||||
|
throw new Error(
|
||||||
|
"WRN-AUTH-CONFIG: AUTH_SECRET must be a non-development secret of at least 32 characters in production",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!config.baseUrl) throw new Error("WRN-AUTH-CONFIG: auth.baseUrl is required in production");
|
||||||
|
const base = new URL(config.baseUrl);
|
||||||
|
if (base.protocol !== "https:")
|
||||||
|
throw new Error("WRN-AUTH-CONFIG: auth.baseUrl must use HTTPS in production");
|
||||||
|
if (["localhost", "127.0.0.1", "::1"].includes(base.hostname)) {
|
||||||
|
throw new Error("WRN-AUTH-CONFIG: auth.baseUrl must not use localhost in production");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
|
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
|
||||||
export interface AuthPluginOptions {
|
export interface AuthPluginOptions {
|
||||||
componentDir?: string;
|
componentDir?: string;
|
||||||
@@ -94,10 +120,10 @@ interface ResolvedAuthConfig {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
csrf: boolean;
|
csrf: boolean;
|
||||||
passkey?: AuthPasskeyHttpOptions;
|
passkey?: AuthPasskeyHttpOptions;
|
||||||
onSignedIn?: AuthSignedInHandler;
|
|
||||||
onSignedOut?: AuthSignedOutHandler;
|
|
||||||
sessionCookieName?: string;
|
sessionCookieName?: string;
|
||||||
onSessionVerification?: AuthSessionVerificationHandler;
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
|
oauth: Record<string, OAuthProvider>;
|
||||||
|
oauthMfaPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleRoot = dirname(fileURLToPath(import.meta.url));
|
const moduleRoot = dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -131,6 +157,24 @@ function resolveConfig(
|
|||||||
const enabled = raw.enabled !== false;
|
const enabled = raw.enabled !== false;
|
||||||
const hasEngine = Boolean(raw.engine);
|
const hasEngine = Boolean(raw.engine);
|
||||||
const hasDefaultDb = Boolean(config.db);
|
const hasDefaultDb = Boolean(config.db);
|
||||||
|
const oauth: Record<string, OAuthProvider> = {};
|
||||||
|
for (const [name, provider] of Object.entries(raw.oauth ?? {})) {
|
||||||
|
if (!provider || !provider.clientId?.trim() || !provider.clientSecret?.trim()) continue;
|
||||||
|
oauth[name] =
|
||||||
|
"authorizeUrl" in provider
|
||||||
|
? provider
|
||||||
|
: name === "google"
|
||||||
|
? google(provider)
|
||||||
|
: name === "github"
|
||||||
|
? github(provider)
|
||||||
|
: name === "discord"
|
||||||
|
? discord(provider)
|
||||||
|
: (() => {
|
||||||
|
throw new Error(
|
||||||
|
`WRN-AUTH-OAUTH-CONFIG: custom provider '${name}' requires a complete OAuthProvider`,
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
@@ -156,10 +200,10 @@ function resolveConfig(
|
|||||||
baseUrl: raw.baseUrl,
|
baseUrl: raw.baseUrl,
|
||||||
csrf: raw.csrf ?? true,
|
csrf: raw.csrf ?? true,
|
||||||
passkey: raw.passkey,
|
passkey: raw.passkey,
|
||||||
onSignedIn: raw.onSignedIn ?? raw.engine?.onSignedIn,
|
|
||||||
onSignedOut: raw.onSignedOut ?? raw.engine?.onSignedOut,
|
|
||||||
sessionCookieName: raw.sessionCookieName,
|
sessionCookieName: raw.sessionCookieName,
|
||||||
onSessionVerification: raw.onSessionVerification,
|
onSessionVerification: raw.onSessionVerification,
|
||||||
|
oauth,
|
||||||
|
oauthMfaPath: raw.oauthMfaPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,6 +232,7 @@ function fallbackConfig(options: AuthPluginOptions): ResolvedAuthConfig {
|
|||||||
schemas: resolveAuthSchemas(),
|
schemas: resolveAuthSchemas(),
|
||||||
|
|
||||||
csrf: true,
|
csrf: true,
|
||||||
|
oauth: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +410,16 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
configure(config, context) {
|
configure(config, context) {
|
||||||
|
const raw = (config.auth ?? {}) as AuthConfig;
|
||||||
|
if (
|
||||||
|
context.mode === "production" &&
|
||||||
|
process.env.NODE_ENV === "production" &&
|
||||||
|
raw.enabled !== false &&
|
||||||
|
raw.engine &&
|
||||||
|
raw.productionValidation !== false
|
||||||
|
) {
|
||||||
|
validateProductionAuthConfig(raw);
|
||||||
|
}
|
||||||
const value = resolveConfig(config, options);
|
const value = resolveConfig(config, options);
|
||||||
|
|
||||||
context.metadata.set(resolvedConfigKey, value);
|
context.metadata.set(resolvedConfigKey, value);
|
||||||
@@ -392,13 +447,11 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
|||||||
|
|
||||||
passkey: value.passkey,
|
passkey: value.passkey,
|
||||||
|
|
||||||
onSignedIn: value.onSignedIn,
|
|
||||||
|
|
||||||
onSignedOut: value.onSignedOut,
|
|
||||||
|
|
||||||
sessionCookieName: value.sessionCookieName,
|
sessionCookieName: value.sessionCookieName,
|
||||||
|
|
||||||
onSessionVerification: value.onSessionVerification,
|
onSessionVerification: value.onSessionVerification,
|
||||||
|
oauth: value.oauth,
|
||||||
|
oauthMfaPath: value.oauthMfaPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
||||||
|
|||||||
@@ -60,9 +60,9 @@ function handlersFor(ctx: Context): AuthHttpHandlers | undefined {
|
|||||||
schemas: getDefaultAuthSchemas(),
|
schemas: getDefaultAuthSchemas(),
|
||||||
baseUrl: routeOptions.baseUrl ?? ctx.url.origin,
|
baseUrl: routeOptions.baseUrl ?? ctx.url.origin,
|
||||||
passkey: routeOptions.passkey,
|
passkey: routeOptions.passkey,
|
||||||
onSignedIn: routeOptions.onSignedIn,
|
|
||||||
onSignedOut: routeOptions.onSignedOut,
|
|
||||||
onSessionVerification: routeOptions.onSessionVerification,
|
onSessionVerification: routeOptions.onSessionVerification,
|
||||||
|
oauth: routeOptions.oauth,
|
||||||
|
oauthMfaPath: routeOptions.oauthMfaPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { invokeAuthHandler } from "../api.ts";
|
||||||
|
|
||||||
|
export function GET(ctx: Context): Promise<Response> {
|
||||||
|
return invokeAuthHandler("completeOAuth", ctx);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { invokeAuthHandler } from "../api.ts";
|
||||||
|
|
||||||
|
export function GET(ctx: Context): Promise<Response> {
|
||||||
|
return invokeAuthHandler("startOAuth", ctx);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { invokeAuthHandler } from "../api.ts";
|
||||||
|
|
||||||
|
export function GET(ctx: Context): Promise<Response> {
|
||||||
|
return invokeAuthHandler("oauthProviders", ctx);
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user