@warlock.js/web 5.1.0 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +174 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +11 -14
- package/esm/build/contribution.mjs +46 -58
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +105 -45
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +47 -13
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +55 -13
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +48 -0
- package/esm/build/page-default-export.mjs.map +1 -0
- package/esm/build/page-routes-manifest.mjs +21 -0
- package/esm/build/page-routes-manifest.mjs.map +1 -0
- package/esm/build/public-files.mjs +45 -0
- package/esm/build/public-files.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +40 -9
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +1 -1
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/index.d.mts +2 -1
- package/esm/client/runtime/index.mjs +2 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +3 -2
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +13 -1
- package/esm/client/runtime/matcher.mjs +13 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +15 -2
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +40 -2
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +4 -2
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +6 -102
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +37 -8
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/loaders.d.mts +7 -36
- package/esm/metadata.d.mts +1 -1
- package/esm/metadata.mjs +1 -1
- package/esm/metadata.mjs.map +1 -1
- package/esm/props.d.mts +16 -8
- package/esm/route.d.mts +2 -17
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +1 -1
- package/esm/routing/url-fragment.mjs.map +1 -1
- package/esm/runtime/register-modules.d.mts +19 -0
- package/esm/runtime/register-modules.mjs +22 -0
- package/esm/runtime/register-modules.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +2 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +2 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +31 -6
- package/esm/server/create-page-route-handler.mjs +128 -43
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +2 -13
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +14 -0
- package/esm/server/error-page.mjs +39 -0
- package/esm/server/error-page.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +7 -4
- package/esm/server/execute-page-request.mjs +79 -80
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +33 -92
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +2 -2
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/index.d.mts +5 -5
- package/esm/server/index.mjs +3 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
- package/esm/server/install-page-routes-from-manifest.mjs +71 -10
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +32 -15
- package/esm/server/install-page-routes.mjs +91 -39
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +3 -36
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +2 -2
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -1
- package/esm/server/not-found-page.mjs +2 -2
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-context.d.mts +3 -9
- package/esm/server/page-context.mjs +3 -16
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +77 -0
- package/esm/server/page-file-change.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +25 -10
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +38 -0
- package/esm/server/page-render-bundle.mjs.map +1 -0
- package/esm/server/page-route-reload.mjs +54 -0
- package/esm/server/page-route-reload.mjs.map +1 -0
- package/esm/server/register-production-public-files.mjs +40 -0
- package/esm/server/register-production-public-files.mjs.map +1 -0
- package/esm/server/render-page.d.mts +11 -14
- package/esm/server/render-page.mjs +154 -28
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +2 -2
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +111 -46
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +41 -30
- package/esm/server/stylesheet-urls.mjs +137 -52
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +42 -0
- package/esm/server/unregistered-pages.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +2 -3
- package/esm/server/web-connector-factory.mjs +7 -6
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +138 -18
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +18 -3
- package/esm/shared.mjs +3 -7
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +2 -2
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +67 -13
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +35 -17
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +6 -6
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +2 -2
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +9 -6
- package/esm/vite/index.mjs +124 -11
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +22 -3
- package/esm/vite/page-registry-plugin.mjs +48 -14
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +2 -2
- package/esm/vite/projection.mjs +38 -15
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +220 -56
- package/llms.txt +5 -3
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +117 -10
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +1 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +14 -8
- package/skills/write-the-root/SKILL.md +3 -1
- package/esm/server/buffered-response.d.mts +0 -58
- package/esm/server/buffered-response.mjs +0 -115
- package/esm/server/buffered-response.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,180 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@warlock.js/web` are documented here.
|
|
4
4
|
|
|
5
|
+
## 5.2.0
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`error.page.tsx`** — the application's one error boundary. It renders when
|
|
10
|
+
a middleware, loader, or component throws; declares no `route`, exactly like
|
|
11
|
+
`404.page.tsx`; and a second `error.page.tsx` anywhere beneath `src/web` is a
|
|
12
|
+
build error. Its component receives `{ error, status }` — the real thrown
|
|
13
|
+
value during SSR, a JSON-safe `{ name, message, stack? }` after hydration.
|
|
14
|
+
`robots: noindex` is a framework default on this path and cannot be
|
|
15
|
+
overridden away. If the failure happens before any page module could load —
|
|
16
|
+
a module-load or `register()` throw — the response falls back further, to a
|
|
17
|
+
framework-owned boundary with no application code at all, and is served
|
|
18
|
+
without a hydration script rather than risk hydrating against markup nothing
|
|
19
|
+
can vouch for.
|
|
20
|
+
- **A page's `route` export is now optional.** A `*.page.tsx` with no `route`
|
|
21
|
+
derives its path and its name from its location beneath `src/web`:
|
|
22
|
+
directories contribute segments, `(group)` directories contribute nothing,
|
|
23
|
+
`index.page.tsx` claims its own directory, and `[id]` becomes `:id`. An
|
|
24
|
+
explicit `route` still always wins over the derived one. This replaces the
|
|
25
|
+
5.1 behaviour, where an omitted `route` threw `MissingRouteExportError` at
|
|
26
|
+
install time — that error class no longer exists.
|
|
27
|
+
- **Live page-route re-registration in `warlock dev`.** Creating, deleting, or
|
|
28
|
+
editing a page's `route` export used to require a manual restart to take
|
|
29
|
+
effect — the route table was built once at boot and never again, so a
|
|
30
|
+
renamed route kept serving its old path and a deleted page kept 404-ing at
|
|
31
|
+
its old URL forever. The dev connector now re-registers the affected routes
|
|
32
|
+
in place, atomically, with no dev-server restart and no loss of Vite's HMR
|
|
33
|
+
state. A component-body-only edit still takes the ordinary Fast Refresh
|
|
34
|
+
path; only membership and route-identity changes go through this path.
|
|
35
|
+
- **A dev-only diagnostic for a page file that exists but isn't reachable.**
|
|
36
|
+
When a request 404s, Warlock checks whether an unregistered `*.page.tsx`
|
|
37
|
+
under `src/web` would have matched it, and if so, warns naming the file.
|
|
38
|
+
This is the case that used to be silent: a page created after boot, or one
|
|
39
|
+
whose `route` was edited to a path nothing else claims, previously 404'd
|
|
40
|
+
with no explanation anywhere in the terminal.
|
|
41
|
+
- **`export const register`** — an optional, synchronous, no-argument hook on
|
|
42
|
+
`root.tsx`, `layout.tsx`, and `*.page.tsx`. It runs once per module
|
|
43
|
+
namespace instance, on both the server and the browser, before that
|
|
44
|
+
module's middleware or loader — the seam for one-time setup a page or
|
|
45
|
+
layout needs on both sides of hydration. It must not return a Promise;
|
|
46
|
+
returning one throws.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Page requests now tolerate one trailing slash identically in development
|
|
51
|
+
and production.** `/about` and `/about/` serve the same page; `/` remains the
|
|
52
|
+
root path and case handling is unchanged. Previously the development
|
|
53
|
+
dispatcher accepted the slash while the production Fastify route returned
|
|
54
|
+
404.
|
|
55
|
+
|
|
56
|
+
- ⚠ **BREAKING — `process.env` is refused entirely in the client/universal
|
|
57
|
+
graph, and there is no `PUBLIC_` exception.** Neither a static key
|
|
58
|
+
(`process.env.PUBLIC_API_URL`) nor a computed one (`process.env[key]`) is
|
|
59
|
+
allowed: `process` does not exist in a browser, so there is no such thing as
|
|
60
|
+
a "public" `process.env` key. **Bare value-reads of the object now fail
|
|
61
|
+
too** — `const { X } = process.env`, `{ ...process.env }`,
|
|
62
|
+
`Object.keys(process.env)`, `JSON.stringify(process.env)`, or passing it as
|
|
63
|
+
an argument — which is the case that previously let an entire server
|
|
64
|
+
environment reach a component in one line while every keyed read was being
|
|
65
|
+
refused. `globalThis.process.env`, `window.process.env` and
|
|
66
|
+
`process["env"]` are matched as well.
|
|
67
|
+
|
|
68
|
+
**Enforcement now covers dev SSR as well as the client bundle, and a
|
|
69
|
+
violation fails the build** rather than being a production-only surprise.
|
|
70
|
+
Files under `node_modules` stay out of scope by design — a dependency's own
|
|
71
|
+
`process.env.NODE_ENV` guard is not the application's problem.
|
|
72
|
+
|
|
73
|
+
⚠ **`env("PUBLIC_X")` does not work client-side either**, and never did:
|
|
74
|
+
`env` comes from `@warlock.js/core`, which declares itself server-only, so
|
|
75
|
+
the import is refused before the call is ever examined. **The supported
|
|
76
|
+
pattern is to read the value in a page loader — server code — and pass it to
|
|
77
|
+
the page as loader data:**
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
export const loader = (async () => ({
|
|
81
|
+
siteName: env("PUBLIC_SITE_NAME"),
|
|
82
|
+
})) satisfies PageLoader;
|
|
83
|
+
|
|
84
|
+
export default function HomePage({ data }: PageProps<typeof loader>) {
|
|
85
|
+
return <h1>{data.siteName}</h1>;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If a value must genuinely be inlined into browser code instead of passed as
|
|
90
|
+
loader data, the one supported spelling is `import.meta.env.PUBLIC_*` with a
|
|
91
|
+
static key — Vite's env surface, baked in at build time, so it cannot vary
|
|
92
|
+
per request. Server-side code is unrestricted.
|
|
93
|
+
|
|
94
|
+
- ⚠ **BREAKING — a `*.page.tsx` with no default export is now a hard
|
|
95
|
+
discovery/build failure, naming the file.** It previously built and
|
|
96
|
+
registered, then served a blank `200` at its URL — a page that looked
|
|
97
|
+
deployed, rendered nothing, and produced no error anywhere.
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
The page "src/web/contact.page.tsx" has no runtime default export. Every
|
|
101
|
+
`*.page.tsx` file must default-export the React component it renders.
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`export { Page as default }` satisfies the rule — the check is for a runtime
|
|
105
|
+
default binding, not for the keyword form. `export default interface Page {}`
|
|
106
|
+
does not: a type-only default is erased and leaves no component behind. A
|
|
107
|
+
file that cannot be parsed reports as a parse failure instead, so a syntax
|
|
108
|
+
error never masquerades as a missing export.
|
|
109
|
+
|
|
110
|
+
- **Initial stylesheet links are route-scoped in development and production.**
|
|
111
|
+
Each response now links the ordered, deduplicated CSS chain for its own
|
|
112
|
+
`[root, ...matched layouts, page]`. Production follows those source entries
|
|
113
|
+
and their static imports in Vite's manifest instead of collecting CSS across
|
|
114
|
+
the whole application; development promotes direct stylesheet imports from
|
|
115
|
+
the matched page and layouts as well as the root. Unrelated page CSS no
|
|
116
|
+
longer ships on every response, and page-local critical CSS no longer waits
|
|
117
|
+
for hydration in development.
|
|
118
|
+
- **The production static-asset refusal now names the working 5.2 alternative.**
|
|
119
|
+
Imported non-stylesheet assets still work under Vite in development but are
|
|
120
|
+
refused by the esbuild server bundle rather than risk a server/client URL
|
|
121
|
+
mismatch. The diagnostic now tells the developer to place the file under the
|
|
122
|
+
application's `public/` directory and reference its root URL
|
|
123
|
+
(`public/logo.svg` → `/logo.svg`) instead of waiting for an unspecified future
|
|
124
|
+
server build. Stylesheet imports remain supported.
|
|
125
|
+
- **Loader execution is sequential, root to leaf, and terminal responses stop
|
|
126
|
+
lower work.** The `root.tsx` App loader runs first, followed by every matched
|
|
127
|
+
layout loader from outermost to innermost, then the page loader. The runtime
|
|
128
|
+
has three top-level slots (`app`, `layout`, `page`), but the layout slot
|
|
129
|
+
composes the full matched layout chain. A page still has at most one
|
|
130
|
+
*rendering* layout; loader-only and middleware-only layouts may appear at
|
|
131
|
+
multiple ancestry levels.
|
|
132
|
+
|
|
133
|
+
⚠ **This package's own documentation previously described the three levels as
|
|
134
|
+
running in parallel, and told you not to rely on ordering between them.** The
|
|
135
|
+
implementation now awaits the App slot, the composed outer-to-inner layout
|
|
136
|
+
slot, and the Page slot in that order.
|
|
137
|
+
|
|
138
|
+
**The first core `Response` a loader returns is terminal**: it stops every
|
|
139
|
+
lower loader from starting, and — because the response is returned whole — it
|
|
140
|
+
also bypasses buffer commit and metadata resolution, discarding the header
|
|
141
|
+
and cookie writes buffered at that same level. A short-circuit
|
|
142
|
+
(`response.redirect()`, `response.notFound()`) commits its own level's buffer
|
|
143
|
+
inclusively and is the right choice when those writes should survive; a throw
|
|
144
|
+
discards the throwing level's buffer and commits only the levels above it.
|
|
145
|
+
|
|
146
|
+
- **Catch-all page routes are documented as unsupported.** `[...slug].page.tsx`
|
|
147
|
+
does not do what it looks like: filesystem routing recognizes only `[name]` as
|
|
148
|
+
a dynamic segment, so `[...slug]` is taken as a **literal** segment and derives
|
|
149
|
+
the path `/docs/[...slug]` and the name `docs.[...slug]` — reachable only at
|
|
150
|
+
the literal URL `/docs/%5B...slug%5D`. ⚠ **Nothing warns about it**: no build
|
|
151
|
+
error, no dev warning, no refusal, just a page that answers a URL nobody will
|
|
152
|
+
request. A real catch-all is deferred; until then use a terminal wildcard with
|
|
153
|
+
an explicit route (`route = { path: "/docs/*" }`). This entry records the gap,
|
|
154
|
+
it does not close it.
|
|
155
|
+
|
|
156
|
+
- **`src/web` is the only page root.** A per-module `src/app/<module>/web/`
|
|
157
|
+
tree is no longer discovered, walked, or installed as a page root by either
|
|
158
|
+
`warlock dev` or `warlock build`. Move any page, layout, or root file that
|
|
159
|
+
lived under `src/app/<module>/web/` into `src/web/` (a subdirectory is
|
|
160
|
+
fine — it still contributes a route segment the same way).
|
|
161
|
+
|
|
162
|
+
### Fixed
|
|
163
|
+
|
|
164
|
+
- **A custom `404.page.tsx` loader no longer executes.** The not-found page
|
|
165
|
+
still registers and renders its real module namespace, but its request
|
|
166
|
+
triple omits the page loader in both development and production. A missing
|
|
167
|
+
URL therefore cannot trigger application data work, redirect, or fail a
|
|
168
|
+
second time through the fallback itself.
|
|
169
|
+
|
|
170
|
+
Precisely what is skipped, because "the 404 page doesn't run loaders" is a
|
|
171
|
+
useful shorthand and not the whole rule: only the **page-level `loader`** is
|
|
172
|
+
omitted. The real module namespace is still used, so `register()` runs and
|
|
173
|
+
the component renders normally, and the page's **middleware still runs**.
|
|
174
|
+
Layout loaders don't run because this page has an empty layout chain by
|
|
175
|
+
construction, not because loaders are disabled on it. And the **`root.tsx`
|
|
176
|
+
App loader does still run** on a 404 request — keep it cheap, and make sure
|
|
177
|
+
it tolerates a request that matched nothing.
|
|
178
|
+
|
|
5
179
|
## 5.1.0
|
|
6
180
|
|
|
7
181
|
> **Upgrade if you installed 5.0.0, 5.0.1 or 5.0.2.** React did not execute at all in
|
package/README.md
CHANGED
|
@@ -41,7 +41,9 @@ export const route = "/products";
|
|
|
41
41
|
export const loader = (async ({ response }) => {
|
|
42
42
|
response.header("cache-control", "private, max-age=60");
|
|
43
43
|
|
|
44
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
products: productResourceCollection(await productsRepository.all()),
|
|
46
|
+
};
|
|
45
47
|
}) satisfies PageLoader;
|
|
46
48
|
|
|
47
49
|
/** Server-only. Runs after the loader and receives its data. */
|
|
@@ -54,7 +56,7 @@ export const metadata = ({ data }) => ({
|
|
|
54
56
|
export default function ProductsPage({ data }: PageProps<typeof loader>) {
|
|
55
57
|
return (
|
|
56
58
|
<ul>
|
|
57
|
-
{data.products.map(product => (
|
|
59
|
+
{data.products.map((product) => (
|
|
58
60
|
<li key={product.id}>{product.name}</li>
|
|
59
61
|
))}
|
|
60
62
|
</ul>
|
|
@@ -76,34 +78,146 @@ production.
|
|
|
76
78
|
compile error.
|
|
77
79
|
- **Loaders that are controllers.** Full request context, guards, DI, and the
|
|
78
80
|
ability to set headers, cookies and status during the render.
|
|
79
|
-
- **`
|
|
80
|
-
re-run.
|
|
81
|
+
- **`refresh()`.** POST to your own API, call it, and the page's loaders
|
|
82
|
+
re-run without pushing history. There is no `revalidate()` export.
|
|
81
83
|
- **Return values are Resources, never models.** A model does not survive the
|
|
82
84
|
wire: it carries methods, a connector handle and every column.
|
|
83
85
|
|
|
84
86
|
## The two halves of a page file
|
|
85
87
|
|
|
86
|
-
| Server-only
|
|
87
|
-
|
|
88
|
-
| `route`, `middleware`, `validation`, `loader`, `metadata` | `
|
|
88
|
+
| Server-only | Runs twice (server + browser) |
|
|
89
|
+
| ---------------------------------------------------------------------------------- | ------------------------------------- |
|
|
90
|
+
| `route`, `middleware`, `validation`, `loader`, `metadata`, `prefix` (layouts only) | default `Page` / `Layout`, `register` |
|
|
89
91
|
|
|
90
92
|
The server half is stripped before anything reaches the browser. The runs-twice
|
|
91
93
|
half never receives `request` or `response` — it also executes in a browser,
|
|
92
94
|
where neither exists — and is never `async`.
|
|
93
95
|
|
|
96
|
+
**A `.client.tsx` suffix is a naming convention, not an SSR-isolation boundary
|
|
97
|
+
in 5.2.** A module statically imported by a root, layout, page, or one of their
|
|
98
|
+
imports is still evaluated by the server. Top-level browser globals such as
|
|
99
|
+
`window` therefore crash SSR boot. Warlock 5.2 does not ship a client-only
|
|
100
|
+
component primitive; code that requires browser globals at module scope cannot
|
|
101
|
+
be part of the SSR page graph.
|
|
102
|
+
|
|
103
|
+
**Imported non-stylesheet static assets are also unsupported by the production
|
|
104
|
+
server build in 5.2.** An import such as `import logo from "./logo.svg"` works
|
|
105
|
+
under Vite in development but is refused by `warlock build`. Put the file under
|
|
106
|
+
the application's `public/` directory and reference its root URL instead:
|
|
107
|
+
`public/logo.svg` is `/logo.svg`. Stylesheet imports remain supported.
|
|
108
|
+
|
|
109
|
+
### Loader order
|
|
110
|
+
|
|
111
|
+
Loaders run **sequentially, root to leaf, each awaited before the next
|
|
112
|
+
starts**: the `root.tsx` App loader, then every matched layout loader from
|
|
113
|
+
outermost to innermost, then the page loader. The runtime has three top-level
|
|
114
|
+
slots (`app`, `layout`, `page`), but the layout slot composes the full matched
|
|
115
|
+
layout chain. Only one layout on that chain may render; loader-only and
|
|
116
|
+
middleware-only layouts still participate.
|
|
117
|
+
|
|
118
|
+
**The first core `Response` returned by a loader is terminal**: it stops every
|
|
119
|
+
lower loader from running, skips metadata, and is sent as-is. Because it is
|
|
120
|
+
returned whole, the buffered header and cookie writes made at that level go
|
|
121
|
+
with it — use `response.redirect()` / `response.notFound()` when you want those
|
|
122
|
+
writes committed, and a raw `Response` only when you mean exactly that
|
|
123
|
+
response.
|
|
124
|
+
|
|
125
|
+
### Environment variables
|
|
126
|
+
|
|
127
|
+
**`process.env` is refused entirely in the client/universal graph. There is no
|
|
128
|
+
`PUBLIC_` exception to it — static or computed — and `env("PUBLIC_X")` does not
|
|
129
|
+
work client-side either.** The supported pattern is to read the value in a page
|
|
130
|
+
loader, which is server code, and pass it to the page as loader data:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { env } from "@warlock.js/core";
|
|
134
|
+
import type { PageLoader, PageProps } from "@warlock.js/web";
|
|
135
|
+
|
|
136
|
+
export const loader = (async () => ({
|
|
137
|
+
siteName: env("PUBLIC_SITE_NAME"),
|
|
138
|
+
})) satisfies PageLoader;
|
|
139
|
+
|
|
140
|
+
export default function HomePage({ data }: PageProps<typeof loader>) {
|
|
141
|
+
return <h1>{data.siteName}</h1>;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The loader is server-only, so it may read configuration normally. Its return
|
|
146
|
+
value becomes page data and is serialized to the browser; return only values
|
|
147
|
+
that are safe to expose.
|
|
148
|
+
|
|
149
|
+
The refusal applies to default page and layout components, `register()`, and
|
|
150
|
+
any helper they import. It covers keyed reads (`process.env.X`,
|
|
151
|
+
`process.env.PUBLIC_X`, `process.env[key]`) and **bare value-reads** of the
|
|
152
|
+
object itself — `const { X } = process.env`, `{ ...process.env }`,
|
|
153
|
+
`Object.keys(process.env)`, `JSON.stringify(process.env)`. `process` does not
|
|
154
|
+
exist in a browser, so touching the object at all is already broken, and
|
|
155
|
+
handing the whole object to a component is how a server secret ships in one
|
|
156
|
+
line.
|
|
157
|
+
|
|
158
|
+
**Enforcement covers dev SSR as well as the client bundle, and a violation
|
|
159
|
+
fails the build** — this is not a production-only check you find out about
|
|
160
|
+
late. Server-side code is unrestricted.
|
|
161
|
+
|
|
162
|
+
If a value must genuinely be inlined into browser code rather than passed as
|
|
163
|
+
loader data, the one supported spelling is `import.meta.env.PUBLIC_*` with a
|
|
164
|
+
static key. That is Vite's surface, not Node's; it is baked in at build time,
|
|
165
|
+
so it cannot vary per request — loader data remains the answer for anything
|
|
166
|
+
request-scoped.
|
|
167
|
+
|
|
168
|
+
## Routing
|
|
169
|
+
|
|
170
|
+
A page's URL is its `route` export when it declares one. A page with no
|
|
171
|
+
`route` derives its URL from its own location beneath `src/web`: directories
|
|
172
|
+
contribute segments, `(group)` directories contribute nothing, `index.page.tsx`
|
|
173
|
+
claims its directory, and `[id]` becomes `:id`. A layout's `prefix` still
|
|
174
|
+
composes in front of either form. `route`, when present, always wins.
|
|
175
|
+
|
|
176
|
+
Every `*.page.tsx` must have a **default export**. A page file with only named
|
|
177
|
+
exports is a hard discovery/build failure naming the file — it used to build
|
|
178
|
+
and then serve a blank `200` at its URL.
|
|
179
|
+
|
|
180
|
+
**Catch-all routes are not supported.** `[...slug].page.tsx` is not a rest
|
|
181
|
+
parameter: only `[name]` is recognized as dynamic, so `[...slug]` is taken as a
|
|
182
|
+
literal segment and derives the unreachable path `/[...slug]`. Nothing warns
|
|
183
|
+
about it. Use a terminal wildcard with an explicit route
|
|
184
|
+
(`route = { path: "/docs/*" }`) until a real catch-all exists.
|
|
185
|
+
|
|
186
|
+
`src/web` is the only page root — a per-module `src/app/<module>/web/` tree is
|
|
187
|
+
not scanned.
|
|
188
|
+
|
|
189
|
+
Incoming URLs may carry one trailing slash: `/about` and `/about/` match the
|
|
190
|
+
same page in development and production. Route declarations remain canonical
|
|
191
|
+
and slash-free, `/` stays the root path, and case handling is unchanged.
|
|
192
|
+
|
|
193
|
+
Exactly two page filenames are special: `404.page.tsx` (the not-found page,
|
|
194
|
+
reached by not matching, never declares `route`, renders with no layout) and
|
|
195
|
+
`error.page.tsx` (the application's one error boundary, also declares no
|
|
196
|
+
`route`). There is no `500.page.tsx`; an unmatched URL is not an error.
|
|
197
|
+
|
|
198
|
+
**`404.page.tsx` never runs its own loader.** The module is registered and
|
|
199
|
+
rendered for real — `register()` and its middleware still run — but the page
|
|
200
|
+
loader is omitted from the request in both development and production, so a
|
|
201
|
+
missing URL cannot trigger application data work, redirect, or fail a second
|
|
202
|
+
time through the fallback. Layout loaders do not run either, because the page
|
|
203
|
+
has an empty layout chain by construction. The `root.tsx` App loader **does**
|
|
204
|
+
still run, so keep it cheap and make sure it tolerates a request that matched
|
|
205
|
+
nothing.
|
|
206
|
+
|
|
207
|
+
In development, creating, deleting, or editing a page's `route` export updates
|
|
208
|
+
the live route table without restarting `warlock dev`.
|
|
209
|
+
|
|
94
210
|
## Where things live
|
|
95
211
|
|
|
96
212
|
```
|
|
97
|
-
src/web/ app-level web layer
|
|
213
|
+
src/web/ app-level web layer and page root
|
|
98
214
|
root.tsx owns <html>, renders #root
|
|
99
215
|
layouts/ shared layouts
|
|
100
216
|
middleware/
|
|
101
|
-
src/app/<module>/web/ that module's own pages
|
|
102
217
|
```
|
|
103
218
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
page layer and leaves a working API.
|
|
219
|
+
All client code lives in `src/web/`, so `rm -rf src/web` removes the page layer
|
|
220
|
+
and leaves a working API.
|
|
107
221
|
|
|
108
222
|
**`web/` means the web layer, not the browser.** Loaders and page middleware
|
|
109
223
|
inside it are server code.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ConnectorBuildContribution } from "@warlock.js/core";
|
|
2
|
+
import { PluginOption } from "vite";
|
|
2
3
|
|
|
3
|
-
//#region
|
|
4
|
+
//#region web/src/build/contribution.d.ts
|
|
4
5
|
type WebBuildOptions = {
|
|
5
6
|
/** Source directory name under the app root. Default: `"src"`. */srcDir?: string;
|
|
6
7
|
/**
|
|
@@ -27,16 +28,6 @@ type WebBuildOptions = {
|
|
|
27
28
|
clientOutDir?: string; /** Client-build resolve aliases: specifier -> absolute path. */
|
|
28
29
|
aliases?: Record<string, string>; /** Extra package names to keep external to the client bundle. */
|
|
29
30
|
external?: string[];
|
|
30
|
-
/**
|
|
31
|
-
* NOT AN APP-FACING OPTION. Set by `webConnector()` from the length of its own
|
|
32
|
-
* `plugins` array; a COUNT rather than the array itself, because this options
|
|
33
|
-
* object is JSON-serializable-values-only (constraint B) and a plugin instance
|
|
34
|
-
* here would drag Vite into every config load.
|
|
35
|
-
*
|
|
36
|
-
* Its only consumer is the refusal in `generate` — see
|
|
37
|
-
* {@link ConnectorPluginsNotSupportedError}.
|
|
38
|
-
*/
|
|
39
|
-
connectorPluginCount?: number;
|
|
40
31
|
};
|
|
41
32
|
/**
|
|
42
33
|
* The resolved `@warlock.js/web` root is not that package.
|
|
@@ -54,12 +45,18 @@ declare class WebPackageRootResolutionError extends Error {
|
|
|
54
45
|
*
|
|
55
46
|
* `generate` writes the page barrel into `ctx.productionDir` and patches
|
|
56
47
|
* esbuild; `emit` produces the client bundle esbuild cannot. The two share one
|
|
57
|
-
* closure
|
|
58
|
-
*
|
|
48
|
+
* closure facts — how many pages exist, the discovered route table, and the
|
|
49
|
+
* exact app-public file list. A build with zero pages skips the hydration
|
|
50
|
+
* bundle but still copies public files when present. `generate` itself does
|
|
59
51
|
* NOT skip: the barrel and its entry import are emitted either way, so the
|
|
60
52
|
* runtime can tell a page-free web build from a build without web.
|
|
53
|
+
*
|
|
54
|
+
* Connector plugins are a separate argument rather than a `WebBuildOptions`
|
|
55
|
+
* field: the app constructs them once, the factory snapshots that array for
|
|
56
|
+
* both dev and build, and this module keeps Vite as a type-only import until
|
|
57
|
+
* `emit` dynamically loads the real client builder.
|
|
61
58
|
*/
|
|
62
|
-
declare function createWebBuildContribution(options?: WebBuildOptions): ConnectorBuildContribution;
|
|
59
|
+
declare function createWebBuildContribution(options?: WebBuildOptions, connectorPlugins?: readonly PluginOption[]): ConnectorBuildContribution;
|
|
63
60
|
//#endregion
|
|
64
61
|
export { WebBuildOptions, WebPackageRootResolutionError, createWebBuildContribution };
|
|
65
62
|
//# sourceMappingURL=contribution.d.mts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region web/src/build/contribution.ts
|
|
5
5
|
/**
|
|
6
6
|
* Web's build-time half — the object a `WebConnector` hands to
|
|
7
7
|
* `warlock build` as its `build` contribution.
|
|
@@ -37,40 +37,6 @@ var ClientOutDirNotSupportedError = class extends Error {
|
|
|
37
37
|
this.name = "ClientOutDirNotSupportedError";
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
|
-
/**
|
|
41
|
-
* `webConnector({ plugins })` was given plugins, and `warlock build` cannot
|
|
42
|
-
* apply them.
|
|
43
|
-
*
|
|
44
|
-
* THE SILENT FAILURE THIS REPLACES. `plugins` reaches exactly one place: the
|
|
45
|
-
* dev server's `createServer({ plugins: [...] })`. The production client bundle
|
|
46
|
-
* is built by `buildWarlockHydrationClient`, which composes its own pipeline
|
|
47
|
-
* (`web/src/vite/index.ts` — projection and the boundary gates) and is not
|
|
48
|
-
* handed the connector's array by anyone. So a plugin worked in `warlock dev`
|
|
49
|
-
* and was absent from `warlock build`, with no warning and a green build log:
|
|
50
|
-
* the site shipped unstyled, or unprocessed in whatever way the plugin
|
|
51
|
-
* mattered, and the build claimed success. That is worse than any loud failure.
|
|
52
|
-
*
|
|
53
|
-
* WHY REFUSE RATHER THAN WIRE IT THROUGH. Plugin ORDER is part of a pipeline's
|
|
54
|
-
* behavior, and a plugin authored against a dev server can misbehave inside a
|
|
55
|
-
* production build; threading the array in is a change to build behavior that
|
|
56
|
-
* has to be verified, not assumed. Refusing removes the silent failure now and
|
|
57
|
-
* is strictly additive to reverse — wiring the plugins through later breaks no
|
|
58
|
-
* app that this error currently stops, while shipping a half-verified plugin
|
|
59
|
-
* pipeline could break every one of them.
|
|
60
|
-
*
|
|
61
|
-
* The message names the option and says what to do instead, because an error
|
|
62
|
-
* that only says "no" costs the reader the same hour it took to find this.
|
|
63
|
-
*/
|
|
64
|
-
var ConnectorPluginsNotSupportedError = class extends Error {
|
|
65
|
-
constructor(pluginCount) {
|
|
66
|
-
super(`"plugins" on the web connector is not supported by \`warlock build\`: webConnector({ plugins }) was given ${pluginCount} plugin${pluginCount === 1 ? "" : "s"}, and they reach the dev server ONLY. The production client bundle is built with the framework's own pipeline and would silently ship without them, so this build refuses rather than emit an artifact that differs from what you saw in \`warlock dev\`.
|
|
67
|
-
|
|
68
|
-
What to do instead: express the transform somewhere BOTH dev and build already read.
|
|
69
|
-
- CSS/PostCSS (Tailwind, autoprefixer, ...): put a \`postcss.config.mjs\` at your app root. Vite loads it automatically in dev and in build — this is how Tailwind is supported today.
|
|
70
|
-
- Otherwise: remove \`plugins\` from webConnector() and open an issue describing what the plugin does, so the production pipeline can support it deliberately.`);
|
|
71
|
-
this.name = "ConnectorPluginsNotSupportedError";
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
40
|
/** The package name every candidate web root must declare to be one. */
|
|
75
41
|
const WEB_PACKAGE_NAME = "@warlock.js/web";
|
|
76
42
|
/**
|
|
@@ -120,45 +86,67 @@ async function resolveWebPackageRoot(configured) {
|
|
|
120
86
|
*
|
|
121
87
|
* `generate` writes the page barrel into `ctx.productionDir` and patches
|
|
122
88
|
* esbuild; `emit` produces the client bundle esbuild cannot. The two share one
|
|
123
|
-
* closure
|
|
124
|
-
*
|
|
89
|
+
* closure facts — how many pages exist, the discovered route table, and the
|
|
90
|
+
* exact app-public file list. A build with zero pages skips the hydration
|
|
91
|
+
* bundle but still copies public files when present. `generate` itself does
|
|
125
92
|
* NOT skip: the barrel and its entry import are emitted either way, so the
|
|
126
93
|
* runtime can tell a page-free web build from a build without web.
|
|
94
|
+
*
|
|
95
|
+
* Connector plugins are a separate argument rather than a `WebBuildOptions`
|
|
96
|
+
* field: the app constructs them once, the factory snapshots that array for
|
|
97
|
+
* both dev and build, and this module keeps Vite as a type-only import until
|
|
98
|
+
* `emit` dynamically loads the real client builder.
|
|
127
99
|
*/
|
|
128
|
-
function createWebBuildContribution(options = {}) {
|
|
100
|
+
function createWebBuildContribution(options = {}, connectorPlugins = []) {
|
|
129
101
|
if (options.clientOutDir !== void 0) throw new ClientOutDirNotSupportedError();
|
|
130
102
|
let pageCount = 0;
|
|
103
|
+
let pageRoutes = {
|
|
104
|
+
version: 1,
|
|
105
|
+
routes: []
|
|
106
|
+
};
|
|
107
|
+
let publicFiles = [];
|
|
131
108
|
return {
|
|
132
109
|
async generate(context) {
|
|
133
|
-
const connectorPluginCount = options.connectorPluginCount ?? 0;
|
|
134
|
-
if (connectorPluginCount > 0) throw new ConnectorPluginsNotSupportedError(connectorPluginCount);
|
|
135
110
|
const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } = await import("./generate-pages-barrel.mjs");
|
|
136
|
-
|
|
111
|
+
const { collectPublicFiles } = await import("./public-files.mjs");
|
|
112
|
+
publicFiles = await collectPublicFiles(path.join(context.appRoot, "public"));
|
|
113
|
+
const result = await generatePagesBarrel({
|
|
137
114
|
appRoot: context.appRoot,
|
|
138
115
|
productionDir: context.productionDir,
|
|
139
116
|
srcDir: options.srcDir,
|
|
140
|
-
clientDir: path.relative(context.appRoot, resolveClientOutDir(context)).split(path.sep).join("/")
|
|
141
|
-
|
|
117
|
+
clientDir: path.relative(context.appRoot, resolveClientOutDir(context)).split(path.sep).join("/"),
|
|
118
|
+
publicFiles
|
|
119
|
+
});
|
|
120
|
+
pageCount = result.pageCount;
|
|
121
|
+
pageRoutes = result.pageRoutes;
|
|
142
122
|
return {
|
|
143
123
|
entryImports: [WEB_ENTRY_IMPORT],
|
|
144
124
|
esbuild: WEB_ESBUILD_PATCH
|
|
145
125
|
};
|
|
146
126
|
},
|
|
147
127
|
async emit(context) {
|
|
148
|
-
if (pageCount
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
128
|
+
if (pageCount > 0) {
|
|
129
|
+
const { buildWarlockHydrationClient } = await import("../vite/index.mjs");
|
|
130
|
+
const { appConventionAliases } = await import("../vite/app-convention-aliases.mjs");
|
|
131
|
+
const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({
|
|
132
|
+
find,
|
|
133
|
+
replacement
|
|
134
|
+
}));
|
|
135
|
+
await buildWarlockHydrationClient({
|
|
136
|
+
appRoot: context.appRoot,
|
|
137
|
+
webRoot: await resolveWebPackageRoot(options.webRoot),
|
|
138
|
+
outDir: resolveClientOutDir(context),
|
|
139
|
+
resolveAliases: [...callerAliases, ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? "src"))],
|
|
140
|
+
external: options.external,
|
|
141
|
+
plugins: connectorPlugins
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (publicFiles.length > 0) {
|
|
145
|
+
const { copyPublicFiles } = await import("./public-files.mjs");
|
|
146
|
+
await copyPublicFiles(path.join(context.appRoot, "public"), path.join(resolveClientOutDir(context), "public"), publicFiles);
|
|
147
|
+
}
|
|
148
|
+
const { writePageRoutesManifest } = await import("./page-routes-manifest.mjs");
|
|
149
|
+
await writePageRoutesManifest(path.resolve(context.appRoot, context.options.outdir), pageRoutes);
|
|
162
150
|
}
|
|
163
151
|
};
|
|
164
152
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contribution.mjs","names":[],"sources":["../../../../../../../web/src/build/contribution.ts"],"sourcesContent":["/**\n * Web's build-time half — the object a `WebConnector` hands to\n * `warlock build` as its `build` contribution.\n *\n * KEPT DELIBERATELY LIGHT: everything this\n * module imports statically is a type or `node:path`. The heavy halves — the\n * filesystem discovery/barrel generator, and the Vite client build — are\n * `await import(...)`ed INSIDE the hooks, so a config file that merely\n * constructs the connector never drags Vite, React or the page graph into its\n * static import graph.\n *\n * {@link WebBuildOptions} carries JSON-SERIALIZABLE VALUES ONLY (constraint\n * B): no plugin instances, no functions, no class instances. Anything heavy is\n * constructed inside a hook after that hook's dynamic import.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type {\n ConnectorBuildContext,\n ConnectorBuildContribution,\n ConnectorBuildGenerateResult,\n} from \"@warlock.js/core\";\n\nexport type WebBuildOptions = {\n /** Source directory name under the app root. Default: `\"src\"`. */\n srcDir?: string;\n /**\n * Absolute path to the `@warlock.js/web` package root, which holds the\n * hydration entry (packaged as `esm/hydration/index.mjs`, with a source\n * fallback at `src/hydration/index.ts` for this checkout).\n *\n * Defaults to this module's own package root, derived from `import.meta.url`\n * at emit time. Set it explicitly when the build process loads this module\n * through a transform that rewrites `import.meta` (e.g. a CJS config\n * pipeline) — the derivation is the only thing here that depends on it.\n *\n * Derived or configured, the root is VERIFIED at emit time against\n * `<root>/package.json`'s `name`; a root that is not `@warlock.js/web`\n * throws {@link WebPackageRootResolutionError} rather than being guessed at.\n */\n webRoot?: string;\n /**\n * NOT SUPPORTED. The client bundle always lands at `<build.outdir>/client`,\n * the layout the runtime half reads (`resolveHydrationClientUrl`) — the\n * runtime does not yet consult a relocated path. Setting this option throws\n * {@link ClientOutDirNotSupportedError}.\n */\n clientOutDir?: string;\n /** Client-build resolve aliases: specifier -> absolute path. */\n aliases?: Record<string, string>;\n /** Extra package names to keep external to the client bundle. */\n external?: string[];\n /**\n * NOT AN APP-FACING OPTION. Set by `webConnector()` from the length of its own\n * `plugins` array; a COUNT rather than the array itself, because this options\n * object is JSON-serializable-values-only (constraint B) and a plugin instance\n * here would drag Vite into every config load.\n *\n * Its only consumer is the refusal in `generate` — see\n * {@link ConnectorPluginsNotSupportedError}.\n */\n connectorPluginCount?: number;\n};\n\nfunction resolveClientOutDir(context: ConnectorBuildContext): string {\n return path.resolve(context.appRoot, context.options.outdir, \"client\");\n}\n\n/**\n * `build.clientOutDir` was set.\n *\n * The production runtime hardcodes where it reads the client bundle from\n * (`<outdir>/client`, via `resolveHydrationClientUrl`) — it does not yet\n * consult the build config for a relocated path. A build that honored a\n * custom `clientOutDir` would therefore produce an artifact the runtime\n * cannot find, silently. Rejected at build start, before any work happens,\n * rather than left to surface later as a missing-bundle failure in\n * production.\n */\nexport class ClientOutDirNotSupportedError extends Error {\n public constructor() {\n super(\n '\"build.clientOutDir\" is not supported: the production server reads the client bundle ' +\n \"from its default location and does not consult this option, so a build honoring it \" +\n \"would produce an artifact production cannot serve correctly. Remove \" +\n '\"build.clientOutDir\" from the build config.',\n );\n this.name = \"ClientOutDirNotSupportedError\";\n }\n}\n\n/**\n * `webConnector({ plugins })` was given plugins, and `warlock build` cannot\n * apply them.\n *\n * THE SILENT FAILURE THIS REPLACES. `plugins` reaches exactly one place: the\n * dev server's `createServer({ plugins: [...] })`. The production client bundle\n * is built by `buildWarlockHydrationClient`, which composes its own pipeline\n * (`web/src/vite/index.ts` — projection and the boundary gates) and is not\n * handed the connector's array by anyone. So a plugin worked in `warlock dev`\n * and was absent from `warlock build`, with no warning and a green build log:\n * the site shipped unstyled, or unprocessed in whatever way the plugin\n * mattered, and the build claimed success. That is worse than any loud failure.\n *\n * WHY REFUSE RATHER THAN WIRE IT THROUGH. Plugin ORDER is part of a pipeline's\n * behavior, and a plugin authored against a dev server can misbehave inside a\n * production build; threading the array in is a change to build behavior that\n * has to be verified, not assumed. Refusing removes the silent failure now and\n * is strictly additive to reverse — wiring the plugins through later breaks no\n * app that this error currently stops, while shipping a half-verified plugin\n * pipeline could break every one of them.\n *\n * The message names the option and says what to do instead, because an error\n * that only says \"no\" costs the reader the same hour it took to find this.\n */\nexport class ConnectorPluginsNotSupportedError extends Error {\n public constructor(pluginCount: number) {\n super(\n `\"plugins\" on the web connector is not supported by \\`warlock build\\`: ` +\n `webConnector({ plugins }) was given ${pluginCount} plugin${pluginCount === 1 ? \"\" : \"s\"}, ` +\n \"and they reach the dev server ONLY. The production client bundle is built with the \" +\n \"framework's own pipeline and would silently ship without them, so this build refuses \" +\n \"rather than emit an artifact that differs from what you saw in `warlock dev`.\\n\\n\" +\n \"What to do instead: express the transform somewhere BOTH dev and build already read.\\n\" +\n \" - CSS/PostCSS (Tailwind, autoprefixer, ...): put a `postcss.config.mjs` at your app \" +\n \"root. Vite loads it automatically in dev and in build — this is how Tailwind is \" +\n \"supported today.\\n\" +\n \" - Otherwise: remove `plugins` from webConnector() and open an issue describing what \" +\n \"the plugin does, so the production pipeline can support it deliberately.\",\n );\n this.name = \"ConnectorPluginsNotSupportedError\";\n }\n}\n\n/** The package name every candidate web root must declare to be one. */\nconst WEB_PACKAGE_NAME = \"@warlock.js/web\";\n\n/**\n * The resolved `@warlock.js/web` root is not that package.\n *\n * Same posture as the manifest resolver's three named errors: a root that\n * cannot be proven is REJECTED, never quietly\n * repaired by walking upwards or falling back to `process.cwd()`. A wrong root\n * would otherwise surface much later as an unintelligible Vite entry failure.\n */\nexport class WebPackageRootResolutionError extends Error {\n public constructor(webRoot: string, reason: string, cause?: unknown) {\n super(\n `Cannot resolve the \"${WEB_PACKAGE_NAME}\" package root: \"${webRoot}\" ${reason}. ` +\n \"Pass `webRoot` explicitly in the web build options to point at the package root \" +\n `whose package.json declares \"name\": \"${WEB_PACKAGE_NAME}\".`,\n { cause },\n );\n this.name = \"WebPackageRootResolutionError\";\n }\n}\n\n/**\n * Proves that `webRoot` really is the `@warlock.js/web` package root by reading\n * `<webRoot>/package.json` and matching its `name`. Returns the path unchanged.\n */\nexport function assertWebPackageRoot(webRoot: string): string {\n const manifestPath = path.join(webRoot, \"package.json\");\n\n let raw: string;\n\n try {\n raw = readFileSync(manifestPath, \"utf-8\");\n } catch (error) {\n throw new WebPackageRootResolutionError(webRoot, \"has no readable package.json\", error);\n }\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n throw new WebPackageRootResolutionError(\n webRoot,\n \"has a package.json that is not valid JSON\",\n error,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new WebPackageRootResolutionError(\n webRoot,\n \"has a package.json whose root is not a JSON object\",\n );\n }\n\n const name = (parsed as { name?: unknown }).name;\n\n if (name !== WEB_PACKAGE_NAME) {\n throw new WebPackageRootResolutionError(\n webRoot,\n `declares package name ${JSON.stringify(name)}, not \"${WEB_PACKAGE_NAME}\"`,\n );\n }\n\n return webRoot;\n}\n\nexport async function resolveWebPackageRoot(configured: string | undefined): Promise<string> {\n if (configured !== undefined) {\n return assertWebPackageRoot(path.resolve(configured));\n }\n\n const { fileURLToPath } = await import(\"node:url\");\n\n // `web/src/build/contribution.ts` published as `web/esm/build/contribution.js`\n // — two levels up is the package root under both layouts.\n return assertWebPackageRoot(\n path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\"),\n );\n}\n\n/**\n * Builds web's `ConnectorBuildContribution`.\n *\n * `generate` writes the page barrel into `ctx.productionDir` and patches\n * esbuild; `emit` produces the client bundle esbuild cannot. The two share one\n * closure fact — how many pages exist — because a build with zero pages must\n * skip the client bundle rather than emit an orphan one. `generate` itself does\n * NOT skip: the barrel and its entry import are emitted either way, so the\n * runtime can tell a page-free web build from a build without web.\n */\nexport function createWebBuildContribution(\n options: WebBuildOptions = {},\n): ConnectorBuildContribution {\n if (options.clientOutDir !== undefined) {\n throw new ClientOutDirNotSupportedError();\n }\n\n let pageCount = 0;\n\n return {\n async generate(context: ConnectorBuildContext): Promise<ConnectorBuildGenerateResult | void> {\n // REFUSED HERE, NOT IN THE CONSTRUCTOR ABOVE, and the difference is the\n // whole of `warlock dev` still working.\n //\n // `webConnector()` — and therefore `createWebBuildContribution()` — runs\n // on every config load, dev included. Throwing at construction the way\n // `clientOutDir` does would make a connector with dev-only plugins fail\n // to boot the dev server those plugins exist for. `generate` is the first\n // hook `warlock build` calls, and nothing but a build calls it: refusing\n // here fails the build before any barrel is written or any bundle emitted,\n // and is unreachable from dev.\n //\n // Unconditional on page count. `emit` skips the client bundle when the app\n // has zero pages, but \"your plugins will not be applied\" is true either\n // way, and a refusal that depends on how many pages you happen to have is\n // a worse contract than one that does not.\n const connectorPluginCount = options.connectorPluginCount ?? 0;\n\n if (connectorPluginCount > 0) {\n throw new ConnectorPluginsNotSupportedError(connectorPluginCount);\n }\n\n const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } = await import(\n \"./generate-pages-barrel\"\n );\n\n const result = await generatePagesBarrel({\n appRoot: context.appRoot,\n productionDir: context.productionDir,\n srcDir: options.srcDir,\n // Derived from `resolveClientOutDir` — the SAME function `emit` passes\n // to the Vite build below — so the path baked into the manifest and\n // the path the bundle is written to are one expression, not two that\n // happen to agree today.\n // POSIX-normalised inline rather than via the generator's `toPosix`:\n // this module's static graph is `node:path` and types only, and the\n // generator is reached by `await import` precisely to keep it that way.\n clientDir: path\n .relative(context.appRoot, resolveClientOutDir(context))\n .split(path.sep)\n .join(\"/\"),\n });\n\n pageCount = result.pageCount;\n\n // Contributed unconditionally, zero pages included: the barrel is always\n // written, and the entry has to IMPORT it for the empty table to reach\n // the runtime. Withholding this line on zero pages would leave the\n // manifest absent, which is the runtime's signal for \"never built with\n // web\" — the exact confusion the always-written barrel removes.\n return { entryImports: [WEB_ENTRY_IMPORT], esbuild: WEB_ESBUILD_PATCH };\n },\n\n async emit(context: ConnectorBuildContext): Promise<void> {\n if (pageCount === 0) {\n return;\n }\n\n const { buildWarlockHydrationClient } = await import(\"../vite\");\n const { appConventionAliases } = await import(\"../vite/app-convention-aliases\");\n\n // The app-tree convention (`web/*`, `app/*`) the dev server installs must\n // also reach the production build, from the SAME definition — see\n // `app-convention-aliases.ts`. Caller-supplied aliases come first so a\n // caller can win a conflict, matching the dev server's ordering.\n const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({\n find,\n replacement,\n }));\n\n await buildWarlockHydrationClient({\n appRoot: context.appRoot,\n webRoot: await resolveWebPackageRoot(options.webRoot),\n outDir: resolveClientOutDir(context),\n resolveAliases: [\n ...callerAliases,\n ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? \"src\")),\n ],\n external: options.external,\n });\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAgEA,SAAS,oBAAoB,SAAwC;CACnE,OAAO,KAAK,QAAQ,QAAQ,SAAS,QAAQ,QAAQ,QAAQ,QAAQ;AACvE;;;;;;;;;;;;AAaA,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,cAAc;EACnB,MACE,6RAIF;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAa,oCAAb,cAAuD,MAAM;CAC3D,AAAO,YAAY,aAAqB;EACtC,MACE,6GACyC,YAAY,SAAS,gBAAgB,IAAI,KAAK,IAAI;;;;iKAU7F;EACA,KAAK,OAAO;CACd;AACF;;AAGA,MAAM,mBAAmB;;;;;;;;;AAUzB,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,YAAY,SAAiB,QAAgB,OAAiB;EACnE,MACE,uBAAuB,iBAAiB,mBAAmB,QAAQ,IAAI,OAAO,2HAEpC,iBAAiB,KAC3D,EAAE,MAAM,CACV;EACA,KAAK,OAAO;CACd;AACF;;;;;AAMA,SAAgB,qBAAqB,SAAyB;CAC5D,MAAM,eAAe,KAAK,KAAK,SAAS,cAAc;CAEtD,IAAI;CAEJ,IAAI;EACF,MAAM,aAAa,cAAc,OAAO;CAC1C,SAAS,OAAO;EACd,MAAM,IAAI,8BAA8B,SAAS,gCAAgC,KAAK;CACxF;CAEA,IAAI;CAEJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,8BACR,SACA,6CACA,KACF;CACF;CAEA,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GACvE,MAAM,IAAI,8BACR,SACA,oDACF;CAGF,MAAM,OAAQ,OAA8B;CAE5C,IAAI,SAAS,kBACX,MAAM,IAAI,8BACR,SACA,yBAAyB,KAAK,UAAU,IAAI,EAAE,SAAS,iBAAiB,EAC1E;CAGF,OAAO;AACT;AAEA,eAAsB,sBAAsB,YAAiD;CAC3F,IAAI,eAAe,QACjB,OAAO,qBAAqB,KAAK,QAAQ,UAAU,CAAC;CAGtD,MAAM,EAAE,kBAAkB,MAAM,OAAO;CAIvC,OAAO,qBACL,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI,CACvE;AACF;;;;;;;;;;;AAYA,SAAgB,2BACd,UAA2B,CAAC,GACA;CAC5B,IAAI,QAAQ,iBAAiB,QAC3B,MAAM,IAAI,8BAA8B;CAG1C,IAAI,YAAY;CAEhB,OAAO;EACL,MAAM,SAAS,SAA8E;GAgB3F,MAAM,uBAAuB,QAAQ,wBAAwB;GAE7D,IAAI,uBAAuB,GACzB,MAAM,IAAI,kCAAkC,oBAAoB;GAGlE,MAAM,EAAE,qBAAqB,kBAAkB,sBAAsB,MAAM,OACzE;GAoBF,aAAY,MAjBS,oBAAoB;IACvC,SAAS,QAAQ;IACjB,eAAe,QAAQ;IACvB,QAAQ,QAAQ;IAQhB,WAAW,KACR,SAAS,QAAQ,SAAS,oBAAoB,OAAO,CAAC,CAAC,CACvD,MAAM,KAAK,GAAG,CAAC,CACf,KAAK,GAAG;GACb,CAAC,EAEiB,CAAC;GAOnB,OAAO;IAAE,cAAc,CAAC,gBAAgB;IAAG,SAAS;GAAkB;EACxE;EAEA,MAAM,KAAK,SAA+C;GACxD,IAAI,cAAc,GAChB;GAGF,MAAM,EAAE,gCAAgC,MAAM,OAAO;GACrD,MAAM,EAAE,yBAAyB,MAAM,OAAO;GAM9C,MAAM,gBAAgB,OAAO,QAAQ,QAAQ,WAAW,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,kBAAkB;IACxF;IACA;GACF,EAAE;GAEF,MAAM,4BAA4B;IAChC,SAAS,QAAQ;IACjB,SAAS,MAAM,sBAAsB,QAAQ,OAAO;IACpD,QAAQ,oBAAoB,OAAO;IACnC,gBAAgB,CACd,GAAG,eACH,GAAG,qBAAqB,KAAK,KAAK,QAAQ,SAAS,QAAQ,UAAU,KAAK,CAAC,CAC7E;IACA,UAAU,QAAQ;GACpB,CAAC;EACH;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"contribution.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/build/contribution.ts"],"sourcesContent":["/**\r\n * Web's build-time half — the object a `WebConnector` hands to\r\n * `warlock build` as its `build` contribution.\r\n *\r\n * KEPT DELIBERATELY LIGHT: everything this\r\n * module imports statically is a type or `node:path`. The heavy halves — the\r\n * filesystem discovery/barrel generator, and the Vite client build — are\r\n * `await import(...)`ed INSIDE the hooks, so a config file that merely\r\n * constructs the connector never drags Vite, React or the page graph into its\r\n * static import graph.\r\n *\r\n * {@link WebBuildOptions} carries JSON-SERIALIZABLE VALUES ONLY (constraint\r\n * B): no plugin instances, no functions, no class instances. Anything heavy is\r\n * constructed inside a hook after that hook's dynamic import.\r\n */\r\nimport { readFileSync } from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type {\r\n ConnectorBuildContext,\r\n ConnectorBuildContribution,\r\n ConnectorBuildGenerateResult,\r\n} from \"@warlock.js/core\";\r\nimport type { PluginOption } from \"vite\";\r\n\r\nexport type WebBuildOptions = {\r\n /** Source directory name under the app root. Default: `\"src\"`. */\r\n srcDir?: string;\r\n /**\r\n * Absolute path to the `@warlock.js/web` package root, which holds the\r\n * hydration entry (packaged as `esm/hydration/index.mjs`, with a source\r\n * fallback at `src/hydration/index.ts` for this checkout).\r\n *\r\n * Defaults to this module's own package root, derived from `import.meta.url`\r\n * at emit time. Set it explicitly when the build process loads this module\r\n * through a transform that rewrites `import.meta` (e.g. a CJS config\r\n * pipeline) — the derivation is the only thing here that depends on it.\r\n *\r\n * Derived or configured, the root is VERIFIED at emit time against\r\n * `<root>/package.json`'s `name`; a root that is not `@warlock.js/web`\r\n * throws {@link WebPackageRootResolutionError} rather than being guessed at.\r\n */\r\n webRoot?: string;\r\n /**\r\n * NOT SUPPORTED. The client bundle always lands at `<build.outdir>/client`,\r\n * the layout the runtime half reads (`resolveHydrationClientUrl`) — the\r\n * runtime does not yet consult a relocated path. Setting this option throws\r\n * {@link ClientOutDirNotSupportedError}.\r\n */\r\n clientOutDir?: string;\r\n /** Client-build resolve aliases: specifier -> absolute path. */\r\n aliases?: Record<string, string>;\r\n /** Extra package names to keep external to the client bundle. */\r\n external?: string[];\r\n};\r\n\r\nfunction resolveClientOutDir(context: ConnectorBuildContext): string {\r\n return path.resolve(context.appRoot, context.options.outdir, \"client\");\r\n}\r\n\r\n/**\r\n * `build.clientOutDir` was set.\r\n *\r\n * The production runtime hardcodes where it reads the client bundle from\r\n * (`<outdir>/client`, via `resolveHydrationClientUrl`) — it does not yet\r\n * consult the build config for a relocated path. A build that honored a\r\n * custom `clientOutDir` would therefore produce an artifact the runtime\r\n * cannot find, silently. Rejected at build start, before any work happens,\r\n * rather than left to surface later as a missing-bundle failure in\r\n * production.\r\n */\r\nexport class ClientOutDirNotSupportedError extends Error {\r\n public constructor() {\r\n super(\r\n '\"build.clientOutDir\" is not supported: the production server reads the client bundle ' +\r\n \"from its default location and does not consult this option, so a build honoring it \" +\r\n \"would produce an artifact production cannot serve correctly. Remove \" +\r\n '\"build.clientOutDir\" from the build config.',\r\n );\r\n this.name = \"ClientOutDirNotSupportedError\";\r\n }\r\n}\r\n\r\n/** The package name every candidate web root must declare to be one. */\r\nconst WEB_PACKAGE_NAME = \"@warlock.js/web\";\r\n\r\n/**\r\n * The resolved `@warlock.js/web` root is not that package.\r\n *\r\n * Same posture as the manifest resolver's three named errors: a root that\r\n * cannot be proven is REJECTED, never quietly\r\n * repaired by walking upwards or falling back to `process.cwd()`. A wrong root\r\n * would otherwise surface much later as an unintelligible Vite entry failure.\r\n */\r\nexport class WebPackageRootResolutionError extends Error {\r\n public constructor(webRoot: string, reason: string, cause?: unknown) {\r\n super(\r\n `Cannot resolve the \"${WEB_PACKAGE_NAME}\" package root: \"${webRoot}\" ${reason}. ` +\r\n \"Pass `webRoot` explicitly in the web build options to point at the package root \" +\r\n `whose package.json declares \"name\": \"${WEB_PACKAGE_NAME}\".`,\r\n { cause },\r\n );\r\n this.name = \"WebPackageRootResolutionError\";\r\n }\r\n}\r\n\r\n/**\r\n * Proves that `webRoot` really is the `@warlock.js/web` package root by reading\r\n * `<webRoot>/package.json` and matching its `name`. Returns the path unchanged.\r\n */\r\nexport function assertWebPackageRoot(webRoot: string): string {\r\n const manifestPath = path.join(webRoot, \"package.json\");\r\n\r\n let raw: string;\r\n\r\n try {\r\n raw = readFileSync(manifestPath, \"utf-8\");\r\n } catch (error) {\r\n throw new WebPackageRootResolutionError(webRoot, \"has no readable package.json\", error);\r\n }\r\n\r\n let parsed: unknown;\r\n\r\n try {\r\n parsed = JSON.parse(raw);\r\n } catch (error) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n \"has a package.json that is not valid JSON\",\r\n error,\r\n );\r\n }\r\n\r\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n \"has a package.json whose root is not a JSON object\",\r\n );\r\n }\r\n\r\n const name = (parsed as { name?: unknown }).name;\r\n\r\n if (name !== WEB_PACKAGE_NAME) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n `declares package name ${JSON.stringify(name)}, not \"${WEB_PACKAGE_NAME}\"`,\r\n );\r\n }\r\n\r\n return webRoot;\r\n}\r\n\r\nexport async function resolveWebPackageRoot(configured: string | undefined): Promise<string> {\r\n if (configured !== undefined) {\r\n return assertWebPackageRoot(path.resolve(configured));\r\n }\r\n\r\n const { fileURLToPath } = await import(\"node:url\");\r\n\r\n // `web/src/build/contribution.ts` published as `web/esm/build/contribution.js`\r\n // — two levels up is the package root under both layouts.\r\n return assertWebPackageRoot(\r\n path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\"),\r\n );\r\n}\r\n\r\n/**\r\n * Builds web's `ConnectorBuildContribution`.\r\n *\r\n * `generate` writes the page barrel into `ctx.productionDir` and patches\r\n * esbuild; `emit` produces the client bundle esbuild cannot. The two share one\r\n * closure facts — how many pages exist, the discovered route table, and the\n * exact app-public file list. A build with zero pages skips the hydration\n * bundle but still copies public files when present. `generate` itself does\n * NOT skip: the barrel and its entry import are emitted either way, so the\n * runtime can tell a page-free web build from a build without web.\n *\r\n * Connector plugins are a separate argument rather than a `WebBuildOptions`\r\n * field: the app constructs them once, the factory snapshots that array for\r\n * both dev and build, and this module keeps Vite as a type-only import until\r\n * `emit` dynamically loads the real client builder.\r\n */\r\nexport function createWebBuildContribution(\r\n options: WebBuildOptions = {},\r\n connectorPlugins: readonly PluginOption[] = [],\r\n): ConnectorBuildContribution {\r\n if (options.clientOutDir !== undefined) {\r\n throw new ClientOutDirNotSupportedError();\r\n }\r\n\r\n let pageCount = 0;\r\n let pageRoutes: import(\"./generate-pages-barrel\").PageRoutesManifest = {\n version: 1,\n routes: [],\n };\n let publicFiles: string[] = [];\n\r\n return {\r\n async generate(context: ConnectorBuildContext): Promise<ConnectorBuildGenerateResult | void> {\r\n const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } =\n await import(\"./generate-pages-barrel\");\n const { collectPublicFiles } = await import(\"./public-files\");\n\n publicFiles = await collectPublicFiles(path.join(context.appRoot, \"public\"));\n\n const result = await generatePagesBarrel({\n appRoot: context.appRoot,\r\n productionDir: context.productionDir,\r\n srcDir: options.srcDir,\r\n // Derived from `resolveClientOutDir` — the SAME function `emit` passes\r\n // to the Vite build below — so the path baked into the manifest and\r\n // the path the bundle is written to are one expression, not two that\r\n // happen to agree today.\r\n // POSIX-normalised inline rather than via the generator's `toPosix`:\r\n // this module's static graph is `node:path` and types only, and the\r\n // generator is reached by `await import` precisely to keep it that way.\r\n clientDir: path\r\n .relative(context.appRoot, resolveClientOutDir(context))\r\n .split(path.sep)\n .join(\"/\"),\n publicFiles,\n });\n\r\n pageCount = result.pageCount;\r\n pageRoutes = result.pageRoutes;\r\n\r\n // Contributed unconditionally, zero pages included: the barrel is always\r\n // written, and the entry has to IMPORT it for the empty table to reach\r\n // the runtime. Withholding this line on zero pages would leave the\r\n // manifest absent, which is the runtime's signal for \"never built with\r\n // web\" — the exact confusion the always-written barrel removes.\r\n return { entryImports: [WEB_ENTRY_IMPORT], esbuild: WEB_ESBUILD_PATCH };\r\n },\r\n\r\n async emit(context: ConnectorBuildContext): Promise<void> {\r\n if (pageCount > 0) {\n const { buildWarlockHydrationClient } = await import(\"../vite\");\r\n const { appConventionAliases } = await import(\"../vite/app-convention-aliases\");\r\n\r\n // The app-tree convention (`web/*`, `app/*`) the dev server installs must\r\n // also reach the production build, from the SAME definition — see\r\n // `app-convention-aliases.ts`. Caller-supplied aliases come first so a\r\n // caller can win a conflict, matching the dev server's ordering.\r\n const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({\r\n find,\r\n replacement,\r\n }));\r\n\r\n await buildWarlockHydrationClient({\r\n appRoot: context.appRoot,\r\n webRoot: await resolveWebPackageRoot(options.webRoot),\r\n outDir: resolveClientOutDir(context),\r\n resolveAliases: [\r\n ...callerAliases,\r\n ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? \"src\")),\r\n ],\r\n external: options.external,\r\n plugins: connectorPlugins,\r\n });\n }\n\n if (publicFiles.length > 0) {\n const { copyPublicFiles } = await import(\"./public-files\");\n await copyPublicFiles(\n path.join(context.appRoot, \"public\"),\n path.join(resolveClientOutDir(context), \"public\"),\n publicFiles,\n );\n }\n\n const { writePageRoutesManifest } = await import(\"./page-routes-manifest\");\n await writePageRoutesManifest(\r\n path.resolve(context.appRoot, context.options.outdir),\r\n pageRoutes,\r\n );\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
|