@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.
Files changed (182) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/esm/build/contribution.d.mts +55 -0
  5. package/esm/build/contribution.mjs +132 -0
  6. package/esm/build/contribution.mjs.map +1 -0
  7. package/esm/build/discover-pages.mjs +294 -0
  8. package/esm/build/discover-pages.mjs.map +1 -0
  9. package/esm/build/generate-client-registry.mjs +123 -0
  10. package/esm/build/generate-client-registry.mjs.map +1 -0
  11. package/esm/build/generate-pages-barrel.mjs +330 -0
  12. package/esm/build/generate-pages-barrel.mjs.map +1 -0
  13. package/esm/build/read-route-exports.mjs +212 -0
  14. package/esm/build/read-route-exports.mjs.map +1 -0
  15. package/esm/client/build-hydrated-tree.mjs +113 -0
  16. package/esm/client/build-hydrated-tree.mjs.map +1 -0
  17. package/esm/client/hydrate-page.mjs +73 -0
  18. package/esm/client/hydrate-page.mjs.map +1 -0
  19. package/esm/client/navigation/current-route.d.mts +91 -0
  20. package/esm/client/navigation/current-route.mjs +66 -0
  21. package/esm/client/navigation/current-route.mjs.map +1 -0
  22. package/esm/client/navigation/fetch-page-data.mjs +97 -0
  23. package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
  24. package/esm/client/navigation/navigation-root.mjs +196 -0
  25. package/esm/client/navigation/navigation-root.mjs.map +1 -0
  26. package/esm/client/navigation/prefetch.mjs +110 -0
  27. package/esm/client/navigation/prefetch.mjs.map +1 -0
  28. package/esm/client/navigation/refresh.d.mts +40 -0
  29. package/esm/client/navigation/refresh.mjs +146 -0
  30. package/esm/client/navigation/refresh.mjs.map +1 -0
  31. package/esm/client/navigation/verbs.d.mts +55 -0
  32. package/esm/client/navigation/verbs.mjs +66 -0
  33. package/esm/client/navigation/verbs.mjs.map +1 -0
  34. package/esm/client/runtime/index.d.mts +4 -0
  35. package/esm/client/runtime/index.mjs +4 -0
  36. package/esm/client/runtime/manifest.d.mts +8 -0
  37. package/esm/client/runtime/manifest.mjs +77 -0
  38. package/esm/client/runtime/manifest.mjs.map +1 -0
  39. package/esm/client/runtime/matcher.d.mts +7 -0
  40. package/esm/client/runtime/matcher.mjs +169 -0
  41. package/esm/client/runtime/matcher.mjs.map +1 -0
  42. package/esm/client/runtime/types.d.mts +22 -0
  43. package/esm/components/default-app.mjs +32 -0
  44. package/esm/components/default-app.mjs.map +1 -0
  45. package/esm/components/document-context.d.mts +1 -0
  46. package/esm/components/document-context.mjs +24 -0
  47. package/esm/components/document-context.mjs.map +1 -0
  48. package/esm/components/head.d.mts +14 -0
  49. package/esm/components/head.mjs +63 -0
  50. package/esm/components/head.mjs.map +1 -0
  51. package/esm/components/link.d.mts +157 -0
  52. package/esm/components/link.mjs +194 -0
  53. package/esm/components/link.mjs.map +1 -0
  54. package/esm/components/scripts.d.mts +14 -0
  55. package/esm/components/scripts.mjs +21 -0
  56. package/esm/components/scripts.mjs.map +1 -0
  57. package/esm/connector/index.d.mts +6 -0
  58. package/esm/connector/index.mjs +7 -0
  59. package/esm/context.d.mts +107 -0
  60. package/esm/hydration/index.d.mts +1 -0
  61. package/esm/hydration/index.mjs +33 -0
  62. package/esm/hydration/index.mjs.map +1 -0
  63. package/esm/hydration-payload.d.mts +1 -0
  64. package/esm/hydration-payload.mjs +72 -0
  65. package/esm/hydration-payload.mjs.map +1 -0
  66. package/esm/index.d.mts +35 -0
  67. package/esm/index.mjs +12 -0
  68. package/esm/loaders.d.mts +47 -0
  69. package/esm/metadata.d.mts +54 -0
  70. package/esm/props.d.mts +42 -0
  71. package/esm/route.d.mts +30 -0
  72. package/esm/routing/compose-route-path.mjs +33 -0
  73. package/esm/routing/compose-route-path.mjs.map +1 -0
  74. package/esm/routing/data-request.mjs +52 -0
  75. package/esm/routing/data-request.mjs.map +1 -0
  76. package/esm/routing/layout-policy.mjs +78 -0
  77. package/esm/routing/layout-policy.mjs.map +1 -0
  78. package/esm/routing/navigator.mjs +21 -0
  79. package/esm/routing/navigator.mjs.map +1 -0
  80. package/esm/routing/query-string.d.mts +250 -0
  81. package/esm/routing/query-string.mjs +340 -0
  82. package/esm/routing/query-string.mjs.map +1 -0
  83. package/esm/routing/route-identity.mjs +68 -0
  84. package/esm/routing/route-identity.mjs.map +1 -0
  85. package/esm/routing/route-table.d.mts +34 -0
  86. package/esm/routing/route-table.mjs +182 -0
  87. package/esm/routing/route-table.mjs.map +1 -0
  88. package/esm/routing/router-events.d.mts +159 -0
  89. package/esm/routing/router-events.mjs +83 -0
  90. package/esm/routing/router-events.mjs.map +1 -0
  91. package/esm/server/buffered-response.mjs +115 -0
  92. package/esm/server/buffered-response.mjs.map +1 -0
  93. package/esm/server/build-hydration-payload.mjs +27 -0
  94. package/esm/server/build-hydration-payload.mjs.map +1 -0
  95. package/esm/server/client-asset-url-prefix.d.mts +22 -0
  96. package/esm/server/client-asset-url-prefix.mjs +23 -0
  97. package/esm/server/client-asset-url-prefix.mjs.map +1 -0
  98. package/esm/server/create-page-module-loader.mjs +49 -0
  99. package/esm/server/create-page-module-loader.mjs.map +1 -0
  100. package/esm/server/create-page-route-handler.mjs +107 -0
  101. package/esm/server/create-page-route-handler.mjs.map +1 -0
  102. package/esm/server/dev-server.mjs +167 -0
  103. package/esm/server/dev-server.mjs.map +1 -0
  104. package/esm/server/execute-page-request.d.mts +1 -0
  105. package/esm/server/execute-page-request.mjs +166 -0
  106. package/esm/server/execute-page-request.mjs.map +1 -0
  107. package/esm/server/execute-page-request.types.d.mts +1 -0
  108. package/esm/server/hydration-client-url.d.mts +37 -0
  109. package/esm/server/hydration-client-url.mjs +101 -0
  110. package/esm/server/hydration-client-url.mjs.map +1 -0
  111. package/esm/server/index.mjs +13 -0
  112. package/esm/server/install-page-routes-from-manifest.mjs +133 -0
  113. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
  114. package/esm/server/install-page-routes.d.mts +1 -0
  115. package/esm/server/install-page-routes.mjs +6 -0
  116. package/esm/server/install-production-page-routes.mjs +55 -0
  117. package/esm/server/install-production-page-routes.mjs.map +1 -0
  118. package/esm/server/match-page-route.mjs +48 -0
  119. package/esm/server/match-page-route.mjs.map +1 -0
  120. package/esm/server/page-context.d.mts +1 -0
  121. package/esm/server/page-context.mjs +33 -0
  122. package/esm/server/page-context.mjs.map +1 -0
  123. package/esm/server/page-manifest.d.mts +102 -0
  124. package/esm/server/page-manifest.mjs +31 -0
  125. package/esm/server/page-manifest.mjs.map +1 -0
  126. package/esm/server/render-page.mjs +214 -0
  127. package/esm/server/render-page.mjs.map +1 -0
  128. package/esm/server/resolve-page-metadata.mjs +33 -0
  129. package/esm/server/resolve-page-metadata.mjs.map +1 -0
  130. package/esm/server/resolve-validation-data.mjs +27 -0
  131. package/esm/server/resolve-validation-data.mjs.map +1 -0
  132. package/esm/server/settle-page-response.d.mts +1 -0
  133. package/esm/server/settle-page-response.mjs +90 -0
  134. package/esm/server/settle-page-response.mjs.map +1 -0
  135. package/esm/server/stylesheet-urls.mjs +65 -0
  136. package/esm/server/stylesheet-urls.mjs.map +1 -0
  137. package/esm/server/web-connector-factory.d.mts +25 -0
  138. package/esm/server/web-connector-factory.mjs +121 -0
  139. package/esm/server/web-connector-factory.mjs.map +1 -0
  140. package/esm/server/web-connector.d.mts +34 -0
  141. package/esm/server/web-connector.mjs +516 -0
  142. package/esm/server/web-connector.mjs.map +1 -0
  143. package/esm/shared.d.mts +22 -0
  144. package/esm/shared.mjs +223 -0
  145. package/esm/shared.mjs.map +1 -0
  146. package/esm/validation.d.mts +31 -0
  147. package/esm/vite/app-convention-aliases.mjs +43 -0
  148. package/esm/vite/app-convention-aliases.mjs.map +1 -0
  149. package/esm/vite/build-client.d.mts +38 -0
  150. package/esm/vite/build-client.mjs +72 -0
  151. package/esm/vite/build-client.mjs.map +1 -0
  152. package/esm/vite/gate-a-resolve.d.mts +57 -0
  153. package/esm/vite/gate-a-resolve.mjs +834 -0
  154. package/esm/vite/gate-a-resolve.mjs.map +1 -0
  155. package/esm/vite/gate-b-secrets.d.mts +57 -0
  156. package/esm/vite/gate-b-secrets.mjs +299 -0
  157. package/esm/vite/gate-b-secrets.mjs.map +1 -0
  158. package/esm/vite/gate-c-verify.d.mts +118 -0
  159. package/esm/vite/gate-c-verify.mjs +263 -0
  160. package/esm/vite/gate-c-verify.mjs.map +1 -0
  161. package/esm/vite/hydration-entries.d.mts +17 -0
  162. package/esm/vite/hydration-entries.mjs +45 -0
  163. package/esm/vite/hydration-entries.mjs.map +1 -0
  164. package/esm/vite/index.d.mts +126 -0
  165. package/esm/vite/index.mjs +146 -0
  166. package/esm/vite/index.mjs.map +1 -0
  167. package/esm/vite/page-registry-plugin.d.mts +43 -0
  168. package/esm/vite/page-registry-plugin.mjs +138 -0
  169. package/esm/vite/page-registry-plugin.mjs.map +1 -0
  170. package/esm/vite/projection.d.mts +31 -0
  171. package/esm/vite/projection.mjs +384 -0
  172. package/esm/vite/projection.mjs.map +1 -0
  173. package/llms-full.txt +1041 -0
  174. package/llms.txt +21 -0
  175. package/package.json +77 -0
  176. package/skills/add-web-to-an-app/SKILL.md +103 -0
  177. package/skills/create-a-page/SKILL.md +141 -0
  178. package/skills/load-page-data/SKILL.md +183 -0
  179. package/skills/navigate-on-the-client/SKILL.md +182 -0
  180. package/skills/serve-styles/SKILL.md +145 -0
  181. package/skills/use-layouts/SKILL.md +121 -0
  182. 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"}