@warlock.js/web 5.0.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 +49 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/esm/build/contribution.d.mts +55 -0
- package/esm/build/contribution.mjs +132 -0
- package/esm/build/contribution.mjs.map +1 -0
- package/esm/build/discover-pages.mjs +294 -0
- package/esm/build/discover-pages.mjs.map +1 -0
- package/esm/build/generate-client-registry.mjs +123 -0
- package/esm/build/generate-client-registry.mjs.map +1 -0
- package/esm/build/generate-pages-barrel.mjs +330 -0
- package/esm/build/generate-pages-barrel.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +212 -0
- package/esm/build/read-route-exports.mjs.map +1 -0
- package/esm/client/build-hydrated-tree.mjs +113 -0
- package/esm/client/build-hydrated-tree.mjs.map +1 -0
- package/esm/client/hydrate-page.mjs +73 -0
- package/esm/client/hydrate-page.mjs.map +1 -0
- package/esm/client/navigation/current-route.d.mts +91 -0
- package/esm/client/navigation/current-route.mjs +66 -0
- package/esm/client/navigation/current-route.mjs.map +1 -0
- package/esm/client/navigation/fetch-page-data.mjs +97 -0
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
- package/esm/client/navigation/navigation-root.mjs +196 -0
- package/esm/client/navigation/navigation-root.mjs.map +1 -0
- package/esm/client/navigation/prefetch.mjs +110 -0
- package/esm/client/navigation/prefetch.mjs.map +1 -0
- package/esm/client/navigation/refresh.d.mts +40 -0
- package/esm/client/navigation/refresh.mjs +146 -0
- package/esm/client/navigation/refresh.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +55 -0
- package/esm/client/navigation/verbs.mjs +66 -0
- package/esm/client/navigation/verbs.mjs.map +1 -0
- package/esm/client/runtime/index.d.mts +4 -0
- package/esm/client/runtime/index.mjs +4 -0
- package/esm/client/runtime/manifest.d.mts +8 -0
- package/esm/client/runtime/manifest.mjs +77 -0
- package/esm/client/runtime/manifest.mjs.map +1 -0
- package/esm/client/runtime/matcher.d.mts +7 -0
- package/esm/client/runtime/matcher.mjs +169 -0
- package/esm/client/runtime/matcher.mjs.map +1 -0
- package/esm/client/runtime/types.d.mts +22 -0
- package/esm/components/default-app.mjs +32 -0
- package/esm/components/default-app.mjs.map +1 -0
- package/esm/components/document-context.d.mts +1 -0
- package/esm/components/document-context.mjs +24 -0
- package/esm/components/document-context.mjs.map +1 -0
- package/esm/components/head.d.mts +14 -0
- package/esm/components/head.mjs +63 -0
- package/esm/components/head.mjs.map +1 -0
- package/esm/components/link.d.mts +157 -0
- package/esm/components/link.mjs +194 -0
- package/esm/components/link.mjs.map +1 -0
- package/esm/components/scripts.d.mts +14 -0
- package/esm/components/scripts.mjs +21 -0
- package/esm/components/scripts.mjs.map +1 -0
- package/esm/connector/index.d.mts +6 -0
- package/esm/connector/index.mjs +7 -0
- package/esm/context.d.mts +107 -0
- package/esm/hydration/index.d.mts +1 -0
- package/esm/hydration/index.mjs +33 -0
- package/esm/hydration/index.mjs.map +1 -0
- package/esm/hydration-payload.d.mts +1 -0
- package/esm/hydration-payload.mjs +72 -0
- package/esm/hydration-payload.mjs.map +1 -0
- package/esm/index.d.mts +35 -0
- package/esm/index.mjs +12 -0
- package/esm/loaders.d.mts +47 -0
- package/esm/metadata.d.mts +54 -0
- package/esm/props.d.mts +42 -0
- package/esm/route.d.mts +30 -0
- package/esm/routing/compose-route-path.mjs +33 -0
- package/esm/routing/compose-route-path.mjs.map +1 -0
- package/esm/routing/data-request.mjs +52 -0
- package/esm/routing/data-request.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +78 -0
- package/esm/routing/layout-policy.mjs.map +1 -0
- package/esm/routing/navigator.mjs +21 -0
- package/esm/routing/navigator.mjs.map +1 -0
- package/esm/routing/query-string.d.mts +250 -0
- package/esm/routing/query-string.mjs +340 -0
- package/esm/routing/query-string.mjs.map +1 -0
- package/esm/routing/route-identity.mjs +68 -0
- package/esm/routing/route-identity.mjs.map +1 -0
- package/esm/routing/route-table.d.mts +34 -0
- package/esm/routing/route-table.mjs +182 -0
- package/esm/routing/route-table.mjs.map +1 -0
- package/esm/routing/router-events.d.mts +159 -0
- package/esm/routing/router-events.mjs +83 -0
- package/esm/routing/router-events.mjs.map +1 -0
- package/esm/server/buffered-response.mjs +115 -0
- package/esm/server/buffered-response.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +27 -0
- package/esm/server/build-hydration-payload.mjs.map +1 -0
- package/esm/server/client-asset-url-prefix.d.mts +22 -0
- package/esm/server/client-asset-url-prefix.mjs +23 -0
- package/esm/server/client-asset-url-prefix.mjs.map +1 -0
- package/esm/server/create-page-module-loader.mjs +49 -0
- package/esm/server/create-page-module-loader.mjs.map +1 -0
- package/esm/server/create-page-route-handler.mjs +107 -0
- package/esm/server/create-page-route-handler.mjs.map +1 -0
- package/esm/server/dev-server.mjs +167 -0
- package/esm/server/dev-server.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +1 -0
- package/esm/server/execute-page-request.mjs +166 -0
- package/esm/server/execute-page-request.mjs.map +1 -0
- package/esm/server/execute-page-request.types.d.mts +1 -0
- package/esm/server/hydration-client-url.d.mts +37 -0
- package/esm/server/hydration-client-url.mjs +101 -0
- package/esm/server/hydration-client-url.mjs.map +1 -0
- package/esm/server/index.mjs +13 -0
- package/esm/server/install-page-routes-from-manifest.mjs +133 -0
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
- package/esm/server/install-page-routes.d.mts +1 -0
- package/esm/server/install-page-routes.mjs +6 -0
- package/esm/server/install-production-page-routes.mjs +55 -0
- package/esm/server/install-production-page-routes.mjs.map +1 -0
- package/esm/server/match-page-route.mjs +48 -0
- package/esm/server/match-page-route.mjs.map +1 -0
- package/esm/server/page-context.d.mts +1 -0
- package/esm/server/page-context.mjs +33 -0
- package/esm/server/page-context.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +102 -0
- package/esm/server/page-manifest.mjs +31 -0
- package/esm/server/page-manifest.mjs.map +1 -0
- package/esm/server/render-page.mjs +214 -0
- package/esm/server/render-page.mjs.map +1 -0
- package/esm/server/resolve-page-metadata.mjs +33 -0
- package/esm/server/resolve-page-metadata.mjs.map +1 -0
- package/esm/server/resolve-validation-data.mjs +27 -0
- package/esm/server/resolve-validation-data.mjs.map +1 -0
- package/esm/server/settle-page-response.d.mts +1 -0
- package/esm/server/settle-page-response.mjs +90 -0
- package/esm/server/settle-page-response.mjs.map +1 -0
- package/esm/server/stylesheet-urls.mjs +65 -0
- package/esm/server/stylesheet-urls.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +25 -0
- package/esm/server/web-connector-factory.mjs +121 -0
- package/esm/server/web-connector-factory.mjs.map +1 -0
- package/esm/server/web-connector.d.mts +34 -0
- package/esm/server/web-connector.mjs +516 -0
- package/esm/server/web-connector.mjs.map +1 -0
- package/esm/shared.d.mts +22 -0
- package/esm/shared.mjs +223 -0
- package/esm/shared.mjs.map +1 -0
- package/esm/validation.d.mts +31 -0
- package/esm/vite/app-convention-aliases.mjs +43 -0
- package/esm/vite/app-convention-aliases.mjs.map +1 -0
- package/esm/vite/build-client.d.mts +38 -0
- package/esm/vite/build-client.mjs +72 -0
- package/esm/vite/build-client.mjs.map +1 -0
- package/esm/vite/gate-a-resolve.d.mts +57 -0
- package/esm/vite/gate-a-resolve.mjs +834 -0
- package/esm/vite/gate-a-resolve.mjs.map +1 -0
- package/esm/vite/gate-b-secrets.d.mts +57 -0
- package/esm/vite/gate-b-secrets.mjs +299 -0
- package/esm/vite/gate-b-secrets.mjs.map +1 -0
- package/esm/vite/gate-c-verify.d.mts +118 -0
- package/esm/vite/gate-c-verify.mjs +263 -0
- package/esm/vite/gate-c-verify.mjs.map +1 -0
- package/esm/vite/hydration-entries.d.mts +17 -0
- package/esm/vite/hydration-entries.mjs +45 -0
- package/esm/vite/hydration-entries.mjs.map +1 -0
- package/esm/vite/index.d.mts +126 -0
- package/esm/vite/index.mjs +146 -0
- package/esm/vite/index.mjs.map +1 -0
- package/esm/vite/page-registry-plugin.d.mts +43 -0
- package/esm/vite/page-registry-plugin.mjs +138 -0
- package/esm/vite/page-registry-plugin.mjs.map +1 -0
- package/esm/vite/projection.d.mts +31 -0
- package/esm/vite/projection.mjs +384 -0
- package/esm/vite/projection.mjs.map +1 -0
- package/llms-full.txt +1041 -0
- package/llms.txt +21 -0
- package/package.json +77 -0
- package/skills/add-web-to-an-app/SKILL.md +103 -0
- package/skills/create-a-page/SKILL.md +141 -0
- package/skills/load-page-data/SKILL.md +183 -0
- package/skills/navigate-on-the-client/SKILL.md +182 -0
- package/skills/serve-styles/SKILL.md +145 -0
- package/skills/use-layouts/SKILL.md +121 -0
- package/skills/write-the-root/SKILL.md +131 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@warlock.js/web` are documented here.
|
|
4
|
+
|
|
5
|
+
## 5.0.0 - 2026-08-25
|
|
6
|
+
|
|
7
|
+
**First published release.**
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- SSR React pages with hydration, client navigation, route metadata, shared data, and Vite integration.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The hydration runtime is packaged as its own public entry, and production/dev route wiring now resolves the packaged client manifest and stylesheets.
|
|
16
|
+
|
|
17
|
+
## 4.16.0
|
|
18
|
+
|
|
19
|
+
**First published release.** The package existed in the monorepo but was absent
|
|
20
|
+
from the release registry, so it had never reached npm.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- SSR React pages served by the Warlock HTTP server. A page route is an ordinary
|
|
25
|
+
Warlock route whose handler renders React instead of returning JSON.
|
|
26
|
+
- Hydration, and client-side navigation via `<Link>` — no document reload, Back
|
|
27
|
+
and Forward included.
|
|
28
|
+
- React Fast Refresh in `warlock dev`, including a server render that reflects
|
|
29
|
+
the edit rather than the pre-edit module.
|
|
30
|
+
- Typed links: `href(name, params, query)`; an unknown route name is a compile
|
|
31
|
+
error.
|
|
32
|
+
- `revalidate()` — re-run the current route's loaders after a mutation.
|
|
33
|
+
- MRR's navigation API mirrored by name (`navigateTo`, `navigateBack`,
|
|
34
|
+
`currentRoute`, `queryString`, …) without depending on that package.
|
|
35
|
+
- `warlock add web` scaffolds `src/web/` and registers the connector.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- `metadata()` no longer runs when a loader rejected. It used to be called with
|
|
40
|
+
`data: undefined` while the type promised otherwise, so a metadata function
|
|
41
|
+
reading its data threw a `TypeError` that **replaced the loader's real error**
|
|
42
|
+
and pointed at the wrong file.
|
|
43
|
+
- Validation reads the same query the loader reads. Stage 4 took `query` and
|
|
44
|
+
`params` from a hand-parsed URL while `body` and `headers` came from the
|
|
45
|
+
request — so `?tags=a&tags=b` reached validation as `"b"`, and a rule on
|
|
46
|
+
`filter.status` never fired because validation saw a key literally named
|
|
47
|
+
`filter[status]`.
|
|
48
|
+
- `href()` emits the query grammar core actually parses; nested objects and
|
|
49
|
+
arrays are no longer destroyed by `String(value)`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Hassan Zohdy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# @warlock.js/web
|
|
2
|
+
|
|
3
|
+
**SSR React pages, served by the Warlock HTTP server.**
|
|
4
|
+
|
|
5
|
+
Warlock is a backend platform that renders React — not a React framework with a
|
|
6
|
+
server bolted on. This package adds a page layer to an existing Warlock app
|
|
7
|
+
without changing what that app already is.
|
|
8
|
+
|
|
9
|
+
> **A page route is an ordinary Warlock route whose handler renders React
|
|
10
|
+
> instead of returning JSON.**
|
|
11
|
+
|
|
12
|
+
One route table. One matcher. One middleware chain. One validation stage. One
|
|
13
|
+
request context. A page and an API endpoint are the same thing up to the point
|
|
14
|
+
where one returns data and the other returns a document.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
warlock add web
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That installs the package, scaffolds `src/web/` with a root and one page, and
|
|
23
|
+
registers the connector in `warlock.config.ts`. Pages are opt-in: a Warlock app
|
|
24
|
+
is an API until you run this.
|
|
25
|
+
|
|
26
|
+
## A page
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
// src/web/products.page.tsx
|
|
30
|
+
import type { PageLoader, PageProps } from "@warlock.js/web";
|
|
31
|
+
import { productsRepository } from "app/products/repositories/products.repository";
|
|
32
|
+
import { productResourceCollection } from "app/products/resources/product.resource";
|
|
33
|
+
|
|
34
|
+
export const route = "/products";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A loader IS a controller: full Warlock context, the same DI, the same guards.
|
|
38
|
+
* It returns data instead of a response — and it may mutate the response on the
|
|
39
|
+
* way past, which a server-component framework structurally cannot do.
|
|
40
|
+
*/
|
|
41
|
+
export const loader = (async ({ response }) => {
|
|
42
|
+
response.header("cache-control", "private, max-age=60");
|
|
43
|
+
|
|
44
|
+
return { products: productResourceCollection(await productsRepository.all()) };
|
|
45
|
+
}) satisfies PageLoader;
|
|
46
|
+
|
|
47
|
+
/** Server-only. Runs after the loader and receives its data. */
|
|
48
|
+
export const metadata = ({ data }) => ({
|
|
49
|
+
title: "Products",
|
|
50
|
+
description: `${data.products.length} in stock`,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/** Runs twice — server render, then hydration. Never `async`. */
|
|
54
|
+
export default function ProductsPage({ data }: PageProps<typeof loader>) {
|
|
55
|
+
return (
|
|
56
|
+
<ul>
|
|
57
|
+
{data.products.map(product => (
|
|
58
|
+
<li key={product.id}>{product.name}</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`warlock dev` serves it. `warlock build` + `warlock start` serves it in
|
|
66
|
+
production.
|
|
67
|
+
|
|
68
|
+
## What you get
|
|
69
|
+
|
|
70
|
+
- **Server-side rendering with hydration.** Not RSC — RSC is a written non-goal.
|
|
71
|
+
- **Client-side navigation.** `<Link>` swaps the page without a document reload;
|
|
72
|
+
Back and Forward included.
|
|
73
|
+
- **React Fast Refresh in dev.** Edit a component, keep your state, and the
|
|
74
|
+
server renders the new output too.
|
|
75
|
+
- **Typed links.** `href(name, params, query)` — an unknown route name is a
|
|
76
|
+
compile error.
|
|
77
|
+
- **Loaders that are controllers.** Full request context, guards, DI, and the
|
|
78
|
+
ability to set headers, cookies and status during the render.
|
|
79
|
+
- **`revalidate()`.** POST to your own API, call it, and the page's loaders
|
|
80
|
+
re-run.
|
|
81
|
+
- **Return values are Resources, never models.** A model does not survive the
|
|
82
|
+
wire: it carries methods, a connector handle and every column.
|
|
83
|
+
|
|
84
|
+
## The two halves of a page file
|
|
85
|
+
|
|
86
|
+
| Server-only | Runs twice (server + browser) |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `route`, `middleware`, `validation`, `loader`, `metadata` | `Layout`, `ErrorBoundary`, default `Page` |
|
|
89
|
+
|
|
90
|
+
The server half is stripped before anything reaches the browser. The runs-twice
|
|
91
|
+
half never receives `request` or `response` — it also executes in a browser,
|
|
92
|
+
where neither exists — and is never `async`.
|
|
93
|
+
|
|
94
|
+
## Where things live
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
src/web/ app-level web layer, and a page root
|
|
98
|
+
root.tsx owns <html>, renders #root
|
|
99
|
+
layouts/ shared layouts
|
|
100
|
+
middleware/
|
|
101
|
+
src/app/<module>/web/ that module's own pages
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Both are page roots. All client code lives in a `web/` folder, and nothing
|
|
105
|
+
outside one may import from one — so `rm -rf src/web src/app/*/web` removes the
|
|
106
|
+
page layer and leaves a working API.
|
|
107
|
+
|
|
108
|
+
**`web/` means the web layer, not the browser.** Loaders and page middleware
|
|
109
|
+
inside it are server code.
|
|
110
|
+
|
|
111
|
+
## Requirements
|
|
112
|
+
|
|
113
|
+
- `@warlock.js/core`
|
|
114
|
+
- `react` and `react-dom` 19+
|
|
115
|
+
- `vite` and `@vitejs/plugin-react` — dev-only, loaded lazily, optional peers
|
|
116
|
+
|
|
117
|
+
## Documentation
|
|
118
|
+
|
|
119
|
+
The reference application in the Warlock repository (`v5/app`) is the worked
|
|
120
|
+
example: layouts, auth-gated pages, forms posting to real controllers, locale
|
|
121
|
+
handling, and error boundaries.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ConnectorBuildContribution } from "@warlock.js/core";
|
|
2
|
+
|
|
3
|
+
//#region ../web/src/build/contribution.d.ts
|
|
4
|
+
type WebBuildOptions = {
|
|
5
|
+
/** Source directory name under the app root. Default: `"src"`. */srcDir?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Absolute path to the `@warlock.js/web` package root, which holds the
|
|
8
|
+
* hydration entry (packaged as `esm/hydration/index.mjs`, with a source
|
|
9
|
+
* fallback at `src/hydration/index.ts` for this checkout).
|
|
10
|
+
*
|
|
11
|
+
* Defaults to this module's own package root, derived from `import.meta.url`
|
|
12
|
+
* at emit time. Set it explicitly when the build process loads this module
|
|
13
|
+
* through a transform that rewrites `import.meta` (e.g. a CJS config
|
|
14
|
+
* pipeline) — the derivation is the only thing here that depends on it.
|
|
15
|
+
*
|
|
16
|
+
* Derived or configured, the root is VERIFIED at emit time against
|
|
17
|
+
* `<root>/package.json`'s `name`; a root that is not `@warlock.js/web`
|
|
18
|
+
* throws {@link WebPackageRootResolutionError} rather than being guessed at.
|
|
19
|
+
*/
|
|
20
|
+
webRoot?: string;
|
|
21
|
+
/**
|
|
22
|
+
* NOT SUPPORTED. The client bundle always lands at `<build.outdir>/client`,
|
|
23
|
+
* the layout the runtime half reads (`resolveHydrationClientUrl`) — the
|
|
24
|
+
* runtime does not yet consult a relocated path. Setting this option throws
|
|
25
|
+
* {@link ClientOutDirNotSupportedError}.
|
|
26
|
+
*/
|
|
27
|
+
clientOutDir?: string; /** Client-build resolve aliases: specifier -> absolute path. */
|
|
28
|
+
aliases?: Record<string, string>; /** Extra package names to keep external to the client bundle. */
|
|
29
|
+
external?: string[];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The resolved `@warlock.js/web` root is not that package.
|
|
33
|
+
*
|
|
34
|
+
* Same posture as the manifest resolver's three named errors: a root that
|
|
35
|
+
* cannot be proven is REJECTED, never quietly
|
|
36
|
+
* repaired by walking upwards or falling back to `process.cwd()`. A wrong root
|
|
37
|
+
* would otherwise surface much later as an unintelligible Vite entry failure.
|
|
38
|
+
*/
|
|
39
|
+
declare class WebPackageRootResolutionError extends Error {
|
|
40
|
+
constructor(webRoot: string, reason: string, cause?: unknown);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds web's `ConnectorBuildContribution`.
|
|
44
|
+
*
|
|
45
|
+
* `generate` writes the page barrel into `ctx.productionDir` and patches
|
|
46
|
+
* esbuild; `emit` produces the client bundle esbuild cannot. The two share one
|
|
47
|
+
* closure fact — how many pages exist — because a build with zero pages must
|
|
48
|
+
* skip the client bundle rather than emit an orphan one. `generate` itself does
|
|
49
|
+
* NOT skip: the barrel and its entry import are emitted either way, so the
|
|
50
|
+
* runtime can tell a page-free web build from a build without web.
|
|
51
|
+
*/
|
|
52
|
+
declare function createWebBuildContribution(options?: WebBuildOptions): ConnectorBuildContribution;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { WebBuildOptions, WebPackageRootResolutionError, createWebBuildContribution };
|
|
55
|
+
//# sourceMappingURL=contribution.d.mts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region ../web/src/build/contribution.ts
|
|
5
|
+
/**
|
|
6
|
+
* Web's build-time half — the object a `WebConnector` hands to
|
|
7
|
+
* `warlock build` as its `build` contribution.
|
|
8
|
+
*
|
|
9
|
+
* KEPT DELIBERATELY LIGHT: everything this
|
|
10
|
+
* module imports statically is a type or `node:path`. The heavy halves — the
|
|
11
|
+
* filesystem discovery/barrel generator, and the Vite client build — are
|
|
12
|
+
* `await import(...)`ed INSIDE the hooks, so a config file that merely
|
|
13
|
+
* constructs the connector never drags Vite, React or the page graph into its
|
|
14
|
+
* static import graph.
|
|
15
|
+
*
|
|
16
|
+
* {@link WebBuildOptions} carries JSON-SERIALIZABLE VALUES ONLY (constraint
|
|
17
|
+
* B): no plugin instances, no functions, no class instances. Anything heavy is
|
|
18
|
+
* constructed inside a hook after that hook's dynamic import.
|
|
19
|
+
*/
|
|
20
|
+
function resolveClientOutDir(context) {
|
|
21
|
+
return path.resolve(context.appRoot, context.options.outdir, "client");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* `build.clientOutDir` was set.
|
|
25
|
+
*
|
|
26
|
+
* The production runtime hardcodes where it reads the client bundle from
|
|
27
|
+
* (`<outdir>/client`, via `resolveHydrationClientUrl`) — it does not yet
|
|
28
|
+
* consult the build config for a relocated path. A build that honored a
|
|
29
|
+
* custom `clientOutDir` would therefore produce an artifact the runtime
|
|
30
|
+
* cannot find, silently. Rejected at build start, before any work happens,
|
|
31
|
+
* rather than left to surface later as a missing-bundle failure in
|
|
32
|
+
* production.
|
|
33
|
+
*/
|
|
34
|
+
var ClientOutDirNotSupportedError = class extends Error {
|
|
35
|
+
constructor() {
|
|
36
|
+
super("\"build.clientOutDir\" is not supported: the production server reads the client bundle from its default location and does not consult this option, so a build honoring it would produce an artifact production cannot serve correctly. Remove \"build.clientOutDir\" from the build config.");
|
|
37
|
+
this.name = "ClientOutDirNotSupportedError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** The package name every candidate web root must declare to be one. */
|
|
41
|
+
const WEB_PACKAGE_NAME = "@warlock.js/web";
|
|
42
|
+
/**
|
|
43
|
+
* The resolved `@warlock.js/web` root is not that package.
|
|
44
|
+
*
|
|
45
|
+
* Same posture as the manifest resolver's three named errors: a root that
|
|
46
|
+
* cannot be proven is REJECTED, never quietly
|
|
47
|
+
* repaired by walking upwards or falling back to `process.cwd()`. A wrong root
|
|
48
|
+
* would otherwise surface much later as an unintelligible Vite entry failure.
|
|
49
|
+
*/
|
|
50
|
+
var WebPackageRootResolutionError = class extends Error {
|
|
51
|
+
constructor(webRoot, reason, cause) {
|
|
52
|
+
super(`Cannot resolve the "${WEB_PACKAGE_NAME}" package root: "${webRoot}" ${reason}. Pass \`webRoot\` explicitly in the web build options to point at the package root whose package.json declares "name": "${WEB_PACKAGE_NAME}".`, { cause });
|
|
53
|
+
this.name = "WebPackageRootResolutionError";
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Proves that `webRoot` really is the `@warlock.js/web` package root by reading
|
|
58
|
+
* `<webRoot>/package.json` and matching its `name`. Returns the path unchanged.
|
|
59
|
+
*/
|
|
60
|
+
function assertWebPackageRoot(webRoot) {
|
|
61
|
+
const manifestPath = path.join(webRoot, "package.json");
|
|
62
|
+
let raw;
|
|
63
|
+
try {
|
|
64
|
+
raw = readFileSync(manifestPath, "utf-8");
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new WebPackageRootResolutionError(webRoot, "has no readable package.json", error);
|
|
67
|
+
}
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(raw);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new WebPackageRootResolutionError(webRoot, "has a package.json that is not valid JSON", error);
|
|
73
|
+
}
|
|
74
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new WebPackageRootResolutionError(webRoot, "has a package.json whose root is not a JSON object");
|
|
75
|
+
const name = parsed.name;
|
|
76
|
+
if (name !== WEB_PACKAGE_NAME) throw new WebPackageRootResolutionError(webRoot, `declares package name ${JSON.stringify(name)}, not "${WEB_PACKAGE_NAME}"`);
|
|
77
|
+
return webRoot;
|
|
78
|
+
}
|
|
79
|
+
async function resolveWebPackageRoot(configured) {
|
|
80
|
+
if (configured !== void 0) return assertWebPackageRoot(path.resolve(configured));
|
|
81
|
+
const { fileURLToPath } = await import("node:url");
|
|
82
|
+
return assertWebPackageRoot(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds web's `ConnectorBuildContribution`.
|
|
86
|
+
*
|
|
87
|
+
* `generate` writes the page barrel into `ctx.productionDir` and patches
|
|
88
|
+
* esbuild; `emit` produces the client bundle esbuild cannot. The two share one
|
|
89
|
+
* closure fact — how many pages exist — because a build with zero pages must
|
|
90
|
+
* skip the client bundle rather than emit an orphan one. `generate` itself does
|
|
91
|
+
* NOT skip: the barrel and its entry import are emitted either way, so the
|
|
92
|
+
* runtime can tell a page-free web build from a build without web.
|
|
93
|
+
*/
|
|
94
|
+
function createWebBuildContribution(options = {}) {
|
|
95
|
+
if (options.clientOutDir !== void 0) throw new ClientOutDirNotSupportedError();
|
|
96
|
+
let pageCount = 0;
|
|
97
|
+
return {
|
|
98
|
+
async generate(context) {
|
|
99
|
+
const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } = await import("./generate-pages-barrel.mjs");
|
|
100
|
+
pageCount = (await generatePagesBarrel({
|
|
101
|
+
appRoot: context.appRoot,
|
|
102
|
+
productionDir: context.productionDir,
|
|
103
|
+
srcDir: options.srcDir,
|
|
104
|
+
clientDir: path.relative(context.appRoot, resolveClientOutDir(context)).split(path.sep).join("/")
|
|
105
|
+
})).pageCount;
|
|
106
|
+
return {
|
|
107
|
+
entryImports: [WEB_ENTRY_IMPORT],
|
|
108
|
+
esbuild: WEB_ESBUILD_PATCH
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
async emit(context) {
|
|
112
|
+
if (pageCount === 0) return;
|
|
113
|
+
const { buildWarlockHydrationClient } = await import("../vite/index.mjs");
|
|
114
|
+
const { appConventionAliases } = await import("../vite/app-convention-aliases.mjs");
|
|
115
|
+
const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({
|
|
116
|
+
find,
|
|
117
|
+
replacement
|
|
118
|
+
}));
|
|
119
|
+
await buildWarlockHydrationClient({
|
|
120
|
+
appRoot: context.appRoot,
|
|
121
|
+
webRoot: await resolveWebPackageRoot(options.webRoot),
|
|
122
|
+
outDir: resolveClientOutDir(context),
|
|
123
|
+
resolveAliases: [...callerAliases, ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? "src"))],
|
|
124
|
+
external: options.external
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
export { WebPackageRootResolutionError, createWebBuildContribution, resolveWebPackageRoot };
|
|
132
|
+
//# sourceMappingURL=contribution.mjs.map
|
|
@@ -0,0 +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\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/** 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 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":";;;;;;;;;;;;;;;;;;;AAsDA,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;;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;GAC3F,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"}
|