@sjcx/vorma 0.0.0-pre.6 → 0.0.0-pre.8

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 (110) hide show
  1. package/LICENSE +197 -1
  2. package/dist/build/assets/css-static-public-assets.d.ts +22 -0
  3. package/dist/build/assets/static-public-asset-plugin.d.ts +20 -0
  4. package/dist/build/assets/static-public-assets.d.ts +44 -0
  5. package/dist/build/build-application.d.ts +18 -0
  6. package/dist/build/{bundle-client.d.ts → bundling/bundle-client.d.ts} +16 -5
  7. package/dist/build/{client-output-hashing.d.ts → bundling/client-output-hashing.d.ts} +1 -1
  8. package/dist/build/bundling/critical-css.d.ts +19 -0
  9. package/dist/build/cli.js +4773 -849
  10. package/dist/build/config/load-config.d.ts +142 -0
  11. package/dist/build/config/resolve-config.d.ts +63 -0
  12. package/dist/build/{source-layout.d.ts → config/source-layout.d.ts} +4 -6
  13. package/dist/build/config/url-pathname.d.ts +13 -0
  14. package/dist/build/config/vite-options.d.ts +80 -0
  15. package/dist/build/dev/application-process.d.ts +18 -0
  16. package/dist/build/dev/artifacts.d.ts +37 -0
  17. package/dist/build/dev/browser-source.d.ts +25 -0
  18. package/dist/build/dev/build.d.ts +33 -0
  19. package/dist/build/dev/cargo-metadata.d.ts +28 -0
  20. package/dist/build/dev/changes.d.ts +11 -0
  21. package/dist/build/dev/client-inputs.d.ts +25 -0
  22. package/dist/build/dev/client.d.ts +39 -0
  23. package/dist/build/dev/dev-protocol.d.ts +23 -0
  24. package/dist/build/dev/generated-paths.d.ts +27 -0
  25. package/dist/build/dev/generation.d.ts +31 -0
  26. package/dist/build/dev/index-html.d.ts +28 -0
  27. package/dist/build/dev/module-server.d.ts +24 -0
  28. package/dist/build/dev/patterns.d.ts +26 -0
  29. package/dist/build/dev/ports.d.ts +25 -0
  30. package/dist/build/dev/server-process.d.ts +34 -0
  31. package/dist/build/dev/server-sources.d.ts +19 -0
  32. package/dist/build/dev/session.d.ts +28 -0
  33. package/dist/build/dev/sources.d.ts +46 -0
  34. package/dist/build/{development-stylesheet.d.ts → dev/stylesheet.d.ts} +4 -4
  35. package/dist/build/dev/watcher.d.ts +30 -0
  36. package/dist/build/dev/work.d.ts +67 -0
  37. package/dist/build/{client-inputs.d.ts → generation/client-view-entries.d.ts} +1 -3
  38. package/dist/build/generation/compile-application.d.ts +28 -0
  39. package/dist/build/generation/generate-application.d.ts +52 -0
  40. package/dist/build/generation/render-client-contracts.d.ts +15 -0
  41. package/dist/build/{render-source-text.d.ts → generation/render-source-text.d.ts} +3 -7
  42. package/dist/build/inspection/http-method.d.ts +4 -0
  43. package/dist/build/inspection/inspect-application.d.ts +25 -0
  44. package/dist/build/inspection/inspect-projects.d.ts +85 -0
  45. package/dist/build/inspection/rust-server.d.ts +57 -0
  46. package/dist/build/inspection/type-declarations.d.ts +20 -0
  47. package/dist/build/inspection/typescript-projects.d.ts +31 -0
  48. package/dist/build/issues.d.ts +25 -0
  49. package/dist/build/{contained-path.d.ts → output/contained-path.d.ts} +3 -13
  50. package/dist/build/output/content-hash.d.ts +9 -0
  51. package/dist/build/output/dist-dir.d.ts +34 -0
  52. package/dist/build/output/output-files.d.ts +23 -0
  53. package/dist/build/output/output-ownership.d.ts +44 -0
  54. package/dist/build/output/published-file-url.d.ts +6 -0
  55. package/dist/build/patterns/nested-route-chains.d.ts +12 -0
  56. package/dist/build/patterns/pattern-language.d.ts +3 -0
  57. package/dist/build/patterns/route-patterns.d.ts +12 -0
  58. package/dist/build/publication/assemble-artifacts.d.ts +37 -0
  59. package/dist/build/{browser-contracts.d.ts → publication/browser-contracts.d.ts} +1 -1
  60. package/dist/build/publication/build-metadata.d.ts +45 -0
  61. package/dist/build/publication/publish-artifacts.d.ts +17 -0
  62. package/dist/config-FbabCxBO.js +56 -0
  63. package/dist/config.d.ts +106 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.js +2 -0
  66. package/dist/plugin-api-Btb0teGk.js +36 -0
  67. package/dist/vite/index.d.ts +14 -3
  68. package/dist/vite/index.js +25 -9
  69. package/dist/vite/plugin-api.d.ts +18 -0
  70. package/package.json +12 -16
  71. package/dist/build/assemble-artifacts.d.ts +0 -23
  72. package/dist/build/client-public-urls.d.ts +0 -20
  73. package/dist/build/compare-text.d.ts +0 -5
  74. package/dist/build/compile-application.d.ts +0 -43
  75. package/dist/build/content-digest.d.ts +0 -7
  76. package/dist/build/critical-css.d.ts +0 -7
  77. package/dist/build/css-public-assets.d.ts +0 -15
  78. package/dist/build/development-artifacts.d.ts +0 -36
  79. package/dist/build/development-browser-source.d.ts +0 -3
  80. package/dist/build/development-build.d.ts +0 -27
  81. package/dist/build/development-changes.d.ts +0 -14
  82. package/dist/build/development-client.d.ts +0 -21
  83. package/dist/build/development-generation.d.ts +0 -20
  84. package/dist/build/development-listener.d.ts +0 -14
  85. package/dist/build/development-patterns.d.ts +0 -5
  86. package/dist/build/development-protocol.d.ts +0 -20
  87. package/dist/build/development-server-process.d.ts +0 -30
  88. package/dist/build/development-sources.d.ts +0 -22
  89. package/dist/build/development-watcher.d.ts +0 -15
  90. package/dist/build/development-work.d.ts +0 -33
  91. package/dist/build/development.d.ts +0 -3
  92. package/dist/build/generate-application.d.ts +0 -62
  93. package/dist/build/index.d.ts +0 -14
  94. package/dist/build/index.js +0 -2
  95. package/dist/build/inspect-css-artifacts.d.ts +0 -11
  96. package/dist/build/inspect-projects.d.ts +0 -68
  97. package/dist/build/load-config.d.ts +0 -57
  98. package/dist/build/output-files.d.ts +0 -8
  99. package/dist/build/output-ownership.d.ts +0 -21
  100. package/dist/build/public-assets.d.ts +0 -32
  101. package/dist/build/publish-artifacts.d.ts +0 -20
  102. package/dist/build/render-client-contracts.d.ts +0 -10
  103. package/dist/build/render-client-transport.d.ts +0 -11
  104. package/dist/build/render-server-generated.d.ts +0 -22
  105. package/dist/build/render-types.d.ts +0 -31
  106. package/dist/build/resolve-config.d.ts +0 -47
  107. package/dist/build/typescript-projects.d.ts +0 -43
  108. package/dist/build-Dy9VE6xO.js +0 -3013
  109. package/dist/build-invocation-D7Fu8gBK.js +0 -4
  110. package/dist/vite/build-invocation.d.ts +0 -4
@@ -1,3013 +0,0 @@
1
- import { t as VORMA_BUILD_PLUGIN_NAME } from "./build-invocation-D7Fu8gBK.js";
2
- import { Effect, FileSystem, Option, Record, Result, Schema } from "effect";
3
- import { build, normalizePath } from "vite";
4
- import { blake3 } from "@noble/hashes/blake3.js";
5
- import { utf8ToBytes } from "@noble/hashes/utils.js";
6
- import { base32nopad } from "@scure/base";
7
- import { ARTIFACT_PROTOCOL, FRAMEWORK_IDENTITY_PROTOCOL, matchesHttpMethod } from "@vorma/protocol";
8
- import { RolldownMagicString } from "rolldown";
9
- import { readFile } from "node:fs/promises";
10
- import { join } from "node:path";
11
- import * as Path$1 from "effect/Path";
12
- import { SyntaxKind, TokenFlags, isAsExpression, isCallExpression, isElementAccessExpression, isExportSpecifier, isIdentifier, isImportClause, isImportSpecifier, isNamespaceImport, isNoSubstitutionTemplateLiteral, isNonNullExpression, isParenthesizedExpression, isPropertyAccessExpression, isSatisfiesExpression, isStringLiteral, isTypeAssertion, isTypeNode } from "typescript/unstable/ast";
13
- import { API, DiagnosticCategory, ElementFlags, SignatureKind, SymbolFlags } from "typescript/unstable/async";
14
- import { createTemplateHead, createTemplateMiddle, createTemplateTail } from "typescript/unstable/ast/factory";
15
- import { compileEndpointPattern, compileUiPattern, createNestedRouteChains, findConflictingDynamicParameterNames, parseUrlPathname } from "@vorma/protocol/path";
16
- import { VormaConfigSchema } from "@vorma/server/config";
17
- import * as FileSystem$1 from "effect/FileSystem";
18
- import { EFFECT_TRANSPORT_TYPES } from "@vorma/protocol/transport";
19
- import { pid, platform } from "node:process";
20
- import { transform } from "lightningcss";
21
- //#region src/build/browser-contracts.ts
22
- /**
23
- * The static contracts on which browser code and learned routing depend.
24
- * Source locations, export names, and runtime implementations are not inputs.
25
- * This projection is hashed during the build and is never sent to the browser.
26
- */
27
- function projectBrowserContracts(inspection) {
28
- const declarations = inspection.serverDeclarations.map((declaration) => {
29
- if (declaration.kind === "UiLoader") return [`UiLoader ${declaration.pattern}`, {
30
- input: declaration.input ?? "undefined",
31
- output: declaration.output
32
- }];
33
- return [`${declaration.method} ${declaration.pattern}`, {
34
- operation: declaration.operation,
35
- inputTransport: declaration.inputTransport,
36
- input: declaration.inputTransport === "formData" ? "FormData" : declaration.input ?? "undefined",
37
- output: declaration.output,
38
- errors: Object.fromEntries(declaration.errors.map((error) => {
39
- return [error.tag, error.fields];
40
- }))
41
- }];
42
- });
43
- return {
44
- declarations: Object.fromEntries(declarations),
45
- typeAliases: inspection.typeAliases
46
- };
47
- }
48
- //#endregion
49
- //#region src/build/client-inputs.ts
50
- /** Runtime peers that must have one browser identity in development and production. */
51
- const CLIENT_SHARED_PACKAGES = [
52
- "effect",
53
- "@effect/atom-react",
54
- "react",
55
- "react-dom"
56
- ];
57
- /** Projects each validated view declaration into one browser entry. */
58
- function getClientViewEntries(generation) {
59
- return generation.model.clientViews.map((view) => {
60
- return {
61
- pattern: view.pattern,
62
- id: view.filePath,
63
- exportName: view.exportName
64
- };
65
- });
66
- }
67
- //#endregion
68
- //#region src/build/content-digest.ts
69
- /** BLAKE3 content identity, encoded as lowercase base32 without padding. */
70
- function toContentDigest(source) {
71
- return encodeContentDigest(blake3(typeof source === "string" ? utf8ToBytes(source) : source));
72
- }
73
- /** Encodes an already computed BLAKE3 digest. */
74
- function encodeContentDigest(digest) {
75
- return base32nopad.encode(digest).toLowerCase();
76
- }
77
- /** Canonical object ordering; array order carries framework semantics. */
78
- function encodeCanonicalJson(value) {
79
- if (Array.isArray(value)) return `[${value.map(encodeCanonicalJson).join(",")}]`;
80
- if (value !== null && typeof value === "object") return `{${Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${encodeCanonicalJson(item)}`).join(",")}}`;
81
- return JSON.stringify(value);
82
- }
83
- //#endregion
84
- //#region src/build/client-public-urls.ts
85
- /** The asset key as the public file map spells it: without a leading slash. */
86
- function normalizePublicAssetKey(key) {
87
- return key.startsWith("/") ? key.slice(1) : key;
88
- }
89
- function createClientPublicUrlPlugin(input) {
90
- let isDevelopment = false;
91
- let projection = preparePublicUrlReplacements(input);
92
- const referenceIds = /* @__PURE__ */ new Map();
93
- const referenceUrls = /* @__PURE__ */ new Map();
94
- return {
95
- name: "vorma:client-public-urls",
96
- api: { updateInputFn(next) {
97
- projection = preparePublicUrlReplacements(next);
98
- } },
99
- enforce: "post",
100
- configResolved(config) {
101
- isDevelopment = config.command === "serve";
102
- },
103
- resolveFileUrl({ referenceId, chunkId, fileName }) {
104
- const url = referenceUrls.get(referenceId);
105
- if (url === void 0) return;
106
- let references = input.referencedAssets.get(chunkId);
107
- if (references === void 0) {
108
- references = /* @__PURE__ */ new Set();
109
- input.referencedAssets.set(chunkId, references);
110
- }
111
- references.add(fileName);
112
- return JSON.stringify(url);
113
- },
114
- async load(id) {
115
- const { assets, sitesByFile, sourceTexts, publicDirectory } = projection;
116
- const queryIndex = id.indexOf("?");
117
- const filePath = queryIndex === -1 ? id : id.slice(0, queryIndex);
118
- const sites = sitesByFile.get(filePath);
119
- if (sites === void 0) return null;
120
- const inspected = sourceTexts.get(filePath);
121
- const current = await readFile(filePath, "utf8");
122
- if (current !== inspected) return this.error(`${filePath} changed after the build read it; run the build again`);
123
- const code = new RolldownMagicString(current);
124
- for (const site of sites) {
125
- const asset = assets.get(normalizePublicAssetKey(site.key));
126
- if (asset === void 0) return this.error(`Unknown public asset: ${site.key}`);
127
- if (isDevelopment) {
128
- code.overwrite(site.start, site.end, JSON.stringify(asset.url));
129
- continue;
130
- }
131
- let referenceId = referenceIds.get(asset.url);
132
- if (referenceId === void 0) {
133
- referenceId = this.emitFile({
134
- type: "asset",
135
- fileName: asset.fileName,
136
- source: await readFile(join(publicDirectory, asset.fileName))
137
- });
138
- referenceIds.set(asset.url, referenceId);
139
- referenceUrls.set(referenceId, asset.url);
140
- }
141
- code.overwrite(site.start, site.end, `import.meta.ROLLUP_FILE_URL_${referenceId}`);
142
- }
143
- return {
144
- code: code.toString(),
145
- map: code.generateMap({
146
- source: id,
147
- includeContent: true,
148
- hires: true
149
- }).toString()
150
- };
151
- }
152
- };
153
- }
154
- function preparePublicUrlReplacements(input) {
155
- const assets = new Map(input.publicAssets.map((asset) => [asset.sourceKey, asset]));
156
- const sitesByFile = /* @__PURE__ */ new Map();
157
- for (const site of input.sites) {
158
- const sites = sitesByFile.get(site.filePath);
159
- if (sites === void 0) sitesByFile.set(site.filePath, [site]);
160
- else sites.push(site);
161
- }
162
- return {
163
- assets,
164
- sitesByFile,
165
- sourceTexts: input.sourceTexts,
166
- publicDirectory: input.publicDirectory
167
- };
168
- }
169
- //#endregion
170
- //#region src/build/css-public-assets.ts
171
- const CSS_URL_PATTERN = /"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|\/\*[\s\S]*?\*\/|\burl\(\s*(?:"((?:\\[\s\S]|[^"\\])*)"|'((?:\\[\s\S]|[^'\\])*)'|((?:\\[\s\S]|[^)\\])*?))\s*\)/gi;
172
- /** Replaces public URLs before Vite hashes CSS; public files are already published. */
173
- function createCssPublicAssets(publicAssets) {
174
- const cssAssets = /* @__PURE__ */ new Set();
175
- const javascriptAssets = /* @__PURE__ */ new Map();
176
- const externalUrls = /* @__PURE__ */ new Set();
177
- function replaceValue(value) {
178
- let error;
179
- const replaced = value.replace(CSS_URL_PATTERN, (matched, doubleQuoted, singleQuoted, unquoted) => {
180
- const token = doubleQuoted ?? singleQuoted ?? unquoted;
181
- if (token === void 0) return matched;
182
- const reference = decodeCssEscapes(token);
183
- if (!reference.startsWith("@public/")) return matched;
184
- const parsed = Result.try({
185
- try: () => {
186
- const url = new URL(reference.slice(8), "http://vorma.invalid/");
187
- return {
188
- key: decodeURIComponent(url.pathname.slice(1)),
189
- suffix: url.search + url.hash
190
- };
191
- },
192
- catch: () => "Invalid public asset URL: " + reference
193
- });
194
- if (Result.isFailure(parsed)) {
195
- error = parsed.failure;
196
- return matched;
197
- }
198
- const { key, suffix } = parsed.success;
199
- const assetUrl = Object.hasOwn(publicAssets.publicFilemap, key) ? publicAssets.publicFilemap[key] : void 0;
200
- if (assetUrl === void 0) {
201
- error = "Unknown public asset: " + reference;
202
- return matched;
203
- }
204
- const resolved = assetUrl + suffix;
205
- externalUrls.add(resolved);
206
- externalUrls.add(decodeURI(resolved));
207
- externalUrls.add(assetUrl);
208
- return "url(\"" + resolved.replace(/["\\\n\r\f]/g, (character) => "\\" + character.charCodeAt(0).toString(16) + " ") + "\")";
209
- });
210
- return error === void 0 ? Result.succeed(replaced) : Result.fail(error);
211
- }
212
- const postcssPlugin = {
213
- postcssPlugin: "vorma:public-urls",
214
- Once(root) {
215
- let error;
216
- root.walkDecls((declaration) => {
217
- const result = replaceValue(declaration.value);
218
- if (Result.isFailure(result)) error = result.failure;
219
- else declaration.value = result.success;
220
- });
221
- return error === void 0 ? void 0 : Promise.reject(new Error(error));
222
- }
223
- };
224
- return {
225
- cssAssets,
226
- javascriptAssets,
227
- plugin: {
228
- name: "vorma:css-public-assets",
229
- enforce: "post",
230
- config(config) {
231
- const external = config.build?.rolldownOptions?.external;
232
- const renderBuiltUrl = config.experimental?.renderBuiltUrl;
233
- return {
234
- css: { postcss: { plugins: [postcssPlugin] } },
235
- build: { rolldownOptions: { external(source, importer, isResolved) {
236
- if (externalUrls.has(source)) return true;
237
- if (typeof external === "function") return external(source, importer, isResolved);
238
- return (Array.isArray(external) ? external : [external]).some((entry) => {
239
- return typeof entry === "string" ? entry === source : entry instanceof RegExp ? entry.test(source) : false;
240
- });
241
- } } },
242
- experimental: { renderBuiltUrl(fileName, context) {
243
- if (context.hostType === "css" && context.type === "asset") cssAssets.add(stripQueryAndFragment(fileName));
244
- const renderedUrl = renderBuiltUrl?.(fileName, context);
245
- if (context.hostType === "js") {
246
- const references = javascriptAssets.get(context.hostId);
247
- const reference = {
248
- fileName,
249
- renderedUrl
250
- };
251
- if (references === void 0) javascriptAssets.set(context.hostId, [reference]);
252
- else references.push(reference);
253
- }
254
- return renderedUrl;
255
- } }
256
- };
257
- }
258
- },
259
- updatePublicAssetsFn(next) {
260
- publicAssets = next;
261
- }
262
- };
263
- }
264
- /** Decodes escapes inside an already identified CSS URL token. */
265
- function decodeCssEscapes(value) {
266
- return value.replace(/\\(?:([0-9a-f]{1,6})(?:\r\n|[ \t\n\r\f])?|(\r\n|[\n\r\f])|([\s\S]))/gi, (_match, hex, newline, escaped) => {
267
- if (hex !== void 0) {
268
- const codePoint = Number.parseInt(hex, 16);
269
- return codePoint === 0 || codePoint > 1114111 || codePoint >= 55296 && codePoint <= 57343 ? "�" : String.fromCodePoint(codePoint);
270
- }
271
- return newline === void 0 ? escaped ?? "" : "";
272
- });
273
- }
274
- function stripQueryAndFragment(fileName) {
275
- const index = fileName.search(/[?#]/);
276
- return index === -1 ? fileName : fileName.slice(0, index);
277
- }
278
- //#endregion
279
- //#region src/build/contained-path.ts
280
- /** True when a `path.relative` result names the root or a descendant of it. */
281
- function containsRelativePath(input) {
282
- return input.relativePath === "" || !input.absolute && input.relativePath !== ".." && !input.relativePath.startsWith(`..${input.separator}`);
283
- }
284
- /**
285
- * A `path.relative` result with platform separators converted to POSIX
286
- * separators, for generated module specifiers, output keys, and build
287
- * identities, which are POSIX even when the build runs on Windows.
288
- */
289
- function toPosixRelativePath(input) {
290
- return input.path.relative(input.from, input.to).split(input.path.sep).join("/");
291
- }
292
- //#endregion
293
- //#region src/build/compare-text.ts
294
- /** Compares source-derived text by Unicode code units, independent of host locale. */
295
- function compareSourceText(input) {
296
- return input.left < input.right ? -1 : input.left > input.right ? 1 : 0;
297
- }
298
- //#endregion
299
- //#region src/build/render-source-text.ts
300
- /** A tab-indented prefix, matching how a person writes generated source. */
301
- function toIndent(depth) {
302
- return " ".repeat(depth);
303
- }
304
- /** Moves multi-line rendered text deeper by `depth` tabs; the first line keeps its position in the line that embeds it. */
305
- function indentRenderedText(text, depth) {
306
- return text.split("\n").join(`\n${toIndent(depth)}`);
307
- }
308
- const IDENTIFIER_PROPERTY_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
309
- function renderPropertyName(name) {
310
- if (name === "__proto__") return "[\"__proto__\"]";
311
- return IDENTIFIER_PROPERTY_NAME.test(name) ? name : JSON.stringify(name);
312
- }
313
- /** A relative module specifier, explicitly relative even when the target's own name starts with a dot. */
314
- function getModuleSpecifier(input) {
315
- return toRelativeSpecifier(toPosixRelativePath({
316
- path: input.path,
317
- from: input.path.dirname(input.fromFilePath),
318
- to: input.toFilePath
319
- }));
320
- }
321
- /** A path is a relative specifier only when it starts with `./` or `../`. */
322
- function toRelativeSpecifier(relativePath) {
323
- return relativePath.startsWith("./") || relativePath.startsWith("../") ? relativePath : `./${relativePath}`;
324
- }
325
- //#endregion
326
- //#region src/build/render-types.ts
327
- /** Removes the checker's missing-value marker, which is distinct from the ordinary `undefined` type. */
328
- async function withoutAbsence(input) {
329
- const members = input.type.isUnionType() ? await input.type.getTypes() : [input.type];
330
- const present = members.filter((member) => {
331
- return !(member.isIntrinsicType() && member.intrinsicName === "undefined" && member.id !== input.undefinedType.id);
332
- });
333
- return present.length === members.length ? void 0 : present;
334
- }
335
- /** The Effect data types the value transport carries, by the module and symbol that declare them. */
336
- const EFFECT_TYPES = [
337
- {
338
- module: "Option",
339
- symbolName: "Option",
340
- kind: "option",
341
- printedName: "Option.Option"
342
- },
343
- {
344
- module: "Redacted",
345
- symbolName: "Redacted",
346
- kind: "redacted",
347
- printedName: "Redacted.Redacted"
348
- },
349
- {
350
- module: "Chunk",
351
- symbolName: "Chunk",
352
- kind: "chunk",
353
- printedName: "Chunk.Chunk"
354
- },
355
- {
356
- module: "Duration",
357
- symbolName: "Duration",
358
- kind: "duration",
359
- printedName: "Duration.Duration"
360
- },
361
- {
362
- module: "BigDecimal",
363
- symbolName: "BigDecimal",
364
- kind: "big-decimal",
365
- printedName: "BigDecimal.BigDecimal"
366
- },
367
- {
368
- module: "DateTime",
369
- symbolName: "Utc",
370
- kind: "date-time-utc",
371
- printedName: "DateTime.Utc"
372
- },
373
- {
374
- module: "DateTime",
375
- symbolName: "Zoned",
376
- kind: "date-time-zoned",
377
- printedName: "DateTime.Zoned"
378
- },
379
- {
380
- module: "HashMap",
381
- symbolName: "HashMap",
382
- kind: "hash-map",
383
- printedName: "HashMap.HashMap"
384
- },
385
- {
386
- module: "HashSet",
387
- symbolName: "HashSet",
388
- kind: "hash-set",
389
- printedName: "HashSet.HashSet"
390
- }
391
- ];
392
- /** Platform types the value transport carries by name; their generic arguments are rendered. */
393
- const PLATFORM_TYPE_NAMES = /* @__PURE__ */ new Set([
394
- "Date",
395
- "URL",
396
- "URLSearchParams",
397
- "RegExp",
398
- "FormData",
399
- "Map",
400
- "ReadonlyMap",
401
- "Set",
402
- "ReadonlySet"
403
- ]);
404
- /** TypeScript's transient-symbol flag for a readonly property of a mapped or instantiated type. */
405
- const CHECK_FLAG_READONLY = 8;
406
- /** Type member order does not depend on source property order. */
407
- function sortProperties(properties) {
408
- return [...properties].sort((left, right) => {
409
- return compareSourceText({
410
- left: left.name,
411
- right: right.name
412
- });
413
- });
414
- }
415
- /** True when a symbol is declared outside the application: in a default library or an installed package. */
416
- async function matchesLibrarySymbol(input) {
417
- const declaration = input.symbol.declarations[0];
418
- if (declaration === void 0) return false;
419
- const declarationPath = String(declaration.path);
420
- const metadata = await input.program.getSourceFileMetadata(declarationPath);
421
- return metadata !== void 0 && (metadata.isDefaultLibrary || metadata.isFromExternalLibrary || declarationPath.includes("/node_modules/"));
422
- }
423
- async function createTypeRenderer(input) {
424
- const { checker, program, emitter } = input;
425
- const undefinedType = await checker.getUndefinedType();
426
- const aliases = /* @__PURE__ */ new Map();
427
- const rendered = [];
428
- const visiting = /* @__PURE__ */ new Set();
429
- const effectModules = /* @__PURE__ */ new Set();
430
- const transportTypeNames = /* @__PURE__ */ new Set();
431
- async function fail(type, reason) {
432
- return Result.fail({
433
- _tag: "UnrenderableContractType",
434
- reason,
435
- typeText: await checker.typeToString(type)
436
- });
437
- }
438
- function getDeclarationPath(symbol) {
439
- const declaration = symbol.declarations[0];
440
- return declaration === void 0 ? void 0 : String(declaration.path);
441
- }
442
- function matchesEffectType(symbol) {
443
- const declarationPath = getDeclarationPath(symbol);
444
- if (declarationPath === void 0) return;
445
- return EFFECT_TYPES.find((entry) => {
446
- return entry.symbolName === symbol.name && declarationPath.endsWith(`/effect/dist/${entry.module}.d.ts`);
447
- });
448
- }
449
- async function matchesReadonlyProperty(property) {
450
- if ((property.checkFlags & CHECK_FLAG_READONLY) !== 0) return true;
451
- const declaration = await property.valueDeclaration?.resolve();
452
- if (declaration === void 0) return false;
453
- const modifiers = declaration.modifiers;
454
- return modifiers !== void 0 && modifiers.some((modifier) => {
455
- return modifier.kind === SyntaxKind.ReadonlyKeyword;
456
- });
457
- }
458
- async function renderTypeArguments(type, depth) {
459
- const parts = [];
460
- for (const argument of await checker.getTypeArguments(type)) {
461
- const text = await renderNode(argument, depth);
462
- if (Result.isFailure(text)) return text;
463
- parts.push(text.success);
464
- }
465
- return Result.succeed(parts.length === 0 ? "" : `<${parts.join(", ")}>`);
466
- }
467
- async function renderNamedType(type, depth) {
468
- const aliasSymbol = await type.getAliasSymbol();
469
- const symbol = await type.getSymbol();
470
- for (const candidate of [aliasSymbol, symbol]) {
471
- if (candidate === void 0) continue;
472
- const effectType = matchesEffectType(candidate);
473
- if (effectType !== void 0) {
474
- effectModules.add(effectType.module);
475
- transportTypeNames.add(effectType.kind);
476
- const argumentTypes = candidate === aliasSymbol ? await type.getAliasTypeArguments() : type.isTypeReference() ? await checker.getTypeArguments(type) : [];
477
- const parts = [];
478
- for (const argument of argumentTypes) {
479
- const text = await renderNode(argument, depth);
480
- if (Result.isFailure(text)) return text;
481
- parts.push(text.success);
482
- }
483
- return Result.succeed(parts.length === 0 ? effectType.printedName : `${effectType.printedName}<${parts.join(", ")}>`);
484
- }
485
- }
486
- if (symbol !== void 0 && PLATFORM_TYPE_NAMES.has(symbol.name) && await matchesLibrarySymbol({
487
- program,
488
- symbol
489
- })) {
490
- const argumentsText = type.isTypeReference() ? await renderTypeArguments(type, depth) : Result.succeed("");
491
- return Result.map(argumentsText, (text) => `${symbol.name}${text}`);
492
- }
493
- return Result.succeed(null);
494
- }
495
- async function renderProperty(input) {
496
- if ((await checker.getSignaturesOfType(input.propertyType, SignatureKind.Call)).length > 0) return await fail(input.propertyType, `the property ${input.property.name} is a function, which cannot cross the wire`);
497
- const optional = (input.property.flags & SymbolFlags.Optional) !== 0;
498
- const text = optional ? await renderOptionalType(input.propertyType, input.depth) : await renderNode(input.propertyType, input.depth);
499
- if (Result.isFailure(text)) return text;
500
- const readonly = await matchesReadonlyProperty(input.property) ? "readonly " : "";
501
- return Result.succeed(`${toIndent(input.depth)}${readonly}${renderPropertyName(input.property.name)}${optional ? "?" : ""}: ${text.success};`);
502
- }
503
- /** A class instance crosses the wire as the record of its schema fields; the fields come from the class's static `fields`. */
504
- async function renderClassInstance(type, classSymbol, depth) {
505
- const constructorType = await checker.getTypeOfSymbol(classSymbol);
506
- const fields = constructorType === void 0 ? void 0 : await checker.getPropertyOfType(constructorType, "fields");
507
- const fieldsType = fields === void 0 ? void 0 : await checker.getTypeOfSymbol(fields);
508
- if (fieldsType === void 0) return await fail(type, `the class ${classSymbol.name} is not an Effect Schema class, so its wire form is unknown`);
509
- const lines = [];
510
- for (const field of sortProperties(await checker.getPropertiesOfType(fieldsType))) {
511
- const property = await checker.getPropertyOfType(type, field.name);
512
- const propertyType = property === void 0 ? void 0 : await checker.getTypeOfSymbol(property);
513
- if (property === void 0 || propertyType === void 0) return await fail(type, `the class ${classSymbol.name} declares the field ${field.name} without a property of that name`);
514
- const line = await renderProperty({
515
- property,
516
- propertyType,
517
- depth: depth + 1
518
- });
519
- if (Result.isFailure(line)) return line;
520
- lines.push(line.success);
521
- }
522
- return Result.succeed(lines.length === 0 ? "{}" : [
523
- "{",
524
- ...lines,
525
- `${toIndent(depth)}}`
526
- ].join("\n"));
527
- }
528
- async function renderObject(type, depth) {
529
- const symbol = await type.getSymbol();
530
- if (symbol !== void 0 && (symbol.flags & SymbolFlags.Class) !== 0) return await renderClassInstance(type, symbol, depth);
531
- if ((await checker.getSignaturesOfType(type, SignatureKind.Call)).length > 0 || (await checker.getSignaturesOfType(type, SignatureKind.Construct)).length > 0) return await fail(type, "a function cannot cross the wire");
532
- const lines = [];
533
- for (const property of sortProperties(await checker.getPropertiesOfType(type))) {
534
- if (String(property.escapedName).startsWith("__@")) continue;
535
- const propertyType = await checker.getTypeOfSymbol(property);
536
- if (propertyType === void 0) return await fail(type, `the property ${property.name} has no type`);
537
- const line = await renderProperty({
538
- property,
539
- propertyType,
540
- depth: depth + 1
541
- });
542
- if (Result.isFailure(line)) return line;
543
- lines.push(line.success);
544
- }
545
- for (const signature of await checker.getIndexInfosOfType(type)) {
546
- const keyText = await renderNode(signature.keyType, depth + 1);
547
- if (Result.isFailure(keyText)) return keyText;
548
- const valueText = await renderNode(signature.valueType, depth + 1);
549
- if (Result.isFailure(valueText)) return valueText;
550
- lines.push(`${toIndent(depth + 1)}${signature.isReadonly ? "readonly " : ""}[key: ${keyText.success}]: ${valueText.success};`);
551
- }
552
- return Result.succeed(lines.length === 0 ? "{}" : [
553
- "{",
554
- ...lines,
555
- `${toIndent(depth)}}`
556
- ].join("\n"));
557
- }
558
- async function renderOptionalType(type, depth) {
559
- const present = await withoutAbsence({
560
- type,
561
- undefinedType
562
- });
563
- return present === void 0 ? await renderNode(type, depth) : present.length === 1 && present[0] !== void 0 ? await renderNode(present[0], depth) : await renderUnion(present, depth, " | ");
564
- }
565
- async function renderTuple(type, depth) {
566
- const target = await type.getTarget();
567
- const members = [];
568
- for (const [index, element] of (await checker.getTypeArguments(type)).entries()) {
569
- const flags = target.elementFlags[index] ?? ElementFlags.Required;
570
- const text = (flags & ElementFlags.Optional) !== 0 ? await renderOptionalType(element, depth) : await renderNode(element, depth);
571
- if (Result.isFailure(text)) return text;
572
- if ((flags & ElementFlags.Rest) !== 0) members.push(`...Array<${text.success}>`);
573
- else if ((flags & ElementFlags.Variadic) !== 0) members.push(`...(${text.success})`);
574
- else if ((flags & ElementFlags.Optional) !== 0) members.push(`(${text.success})?`);
575
- else members.push(text.success);
576
- }
577
- return Result.succeed(`${target.readonly ? "readonly " : ""}[${members.join(", ")}]`);
578
- }
579
- async function renderUnion(members, depth, separator) {
580
- const parts = [];
581
- let sawTrue = false;
582
- let sawFalse = false;
583
- for (const member of members) {
584
- if (member.isBooleanLiteralType()) {
585
- if (member.value) sawTrue = true;
586
- else sawFalse = true;
587
- continue;
588
- }
589
- const text = await renderNode(member, depth);
590
- if (Result.isFailure(text)) return text;
591
- parts.push(separator === " & " && member.isUnionType() ? `(${text.success})` : text.success);
592
- }
593
- if (sawTrue && sawFalse) parts.push("boolean");
594
- else if (sawTrue) parts.push("true");
595
- else if (sawFalse) parts.push("false");
596
- parts.sort((left, right) => {
597
- return compareSourceText({
598
- left,
599
- right
600
- });
601
- });
602
- return Result.succeed(parts.length === 0 ? "never" : parts.join(separator));
603
- }
604
- /** Keeps literal contents independent of indentation in the containing declaration. */
605
- async function renderTemplateToken(node) {
606
- return (await emitter.printNode(node)).replaceAll("\r", "\\r").replaceAll("\n", "\\n");
607
- }
608
- async function renderTemplateLiteral(type, depth) {
609
- if (!type.isTemplateLiteralType()) return await fail(type, "not a template literal type");
610
- const spans = [];
611
- for (const span of await type.getTypes()) {
612
- const text = await renderNode(span, depth);
613
- if (Result.isFailure(text)) return text;
614
- spans.push(text.success);
615
- }
616
- const [head, ...tail] = type.texts;
617
- let text = await renderTemplateToken(createTemplateHead(head ?? "", "", TokenFlags.None));
618
- for (const [index, span] of spans.entries()) {
619
- const createLiteral = index === spans.length - 1 ? createTemplateTail : createTemplateMiddle;
620
- text += span + await renderTemplateToken(createLiteral(tail[index] ?? "", "", TokenFlags.None));
621
- }
622
- return Result.succeed(text);
623
- }
624
- async function renderNode(type, depth) {
625
- const existingAlias = aliases.get(type.id);
626
- if (existingAlias !== void 0) return Result.succeed(existingAlias);
627
- if (visiting.has(type.id)) {
628
- const name = `Recursive${aliases.size}`;
629
- aliases.set(type.id, name);
630
- return Result.succeed(name);
631
- }
632
- visiting.add(type.id);
633
- const text = await renderTypeStructure(type, depth);
634
- visiting.delete(type.id);
635
- if (Result.isFailure(text)) return text;
636
- const createdAlias = aliases.get(type.id);
637
- if (createdAlias === void 0) return text;
638
- const declared = depth === 0 ? text : await renderTypeStructure(type, 0);
639
- if (Result.isFailure(declared)) return declared;
640
- rendered.push(`type ${createdAlias} = ${declared.success};`);
641
- return Result.succeed(createdAlias);
642
- }
643
- async function renderTypeStructure(type, depth) {
644
- if (type.isErrorType()) return await fail(type, "the type could not be resolved");
645
- if (type.isStringLiteralType()) return Result.succeed(JSON.stringify(type.value));
646
- if (type.isNumberLiteralType()) return Result.succeed(String(type.value));
647
- if (type.isBigIntLiteralType()) return Result.succeed(`${type.value}n`);
648
- if (type.isBooleanLiteralType()) return Result.succeed(type.value ? "true" : "false");
649
- if (type.isIntrinsicType()) return Result.succeed(type.intrinsicName);
650
- const named = await renderNamedType(type, depth);
651
- if (Result.isFailure(named)) return Result.fail(named.failure);
652
- if (named.success !== null) return Result.succeed(named.success);
653
- if (type.isTemplateLiteralType()) return await renderTemplateLiteral(type, depth);
654
- if (type.isUnionType()) return await renderUnion(await type.getTypes(), depth, " | ");
655
- if (type.isIntersectionType()) return await renderUnion(await type.getTypes(), depth, " & ");
656
- if (type.isTypeParameter()) return await fail(type, "a contract type cannot be generic");
657
- if (!type.isObjectType()) return await fail(type, "the type has no wire form");
658
- if (await checker.isArrayType(type) && type.isTypeReference()) {
659
- const [element] = await checker.getTypeArguments(type);
660
- if (element === void 0) return await fail(type, "an array type without an element type");
661
- const text = await renderNode(element, depth);
662
- if (Result.isFailure(text)) return text;
663
- const wrapper = (await type.getSymbol())?.name === "ReadonlyArray" ? "ReadonlyArray" : "Array";
664
- return Result.succeed(`${wrapper}<${text.success}>`);
665
- }
666
- if (await checker.isTupleType(type) && type.isTypeReference()) return await renderTuple(type, depth);
667
- return await renderObject(type, depth);
668
- }
669
- return {
670
- renderType: (input) => renderNode(input.type, input.depth),
671
- renderAliases: () => rendered,
672
- getEffectModules: () => [...effectModules].sort(),
673
- getTransportTypeNames: () => transportTypeNames
674
- };
675
- }
676
- //#endregion
677
- //#region src/build/resolve-config.ts
678
- /** The build excludes both projects' generated directories from application source. */
679
- function getGeneratedSourceDirectories(config) {
680
- return [config.serverGeneratedDirectory, config.clientGeneratedDirectory];
681
- }
682
- /** Derives generated source, public assets, and build coordination from the tsconfig directories. */
683
- function getVormaOwnedPaths(input) {
684
- const { path } = input;
685
- const name = ARTIFACT_PROTOCOL.vormaDirectoryName;
686
- const clientGeneratedDirectory = path.join(input.clientProjectDirectory, name);
687
- const serverGeneratedDirectory = path.join(input.serverProjectDirectory, name);
688
- return {
689
- publicDirectory: path.join(clientGeneratedDirectory, ARTIFACT_PROTOCOL.publicDirectoryName),
690
- ignoreFilePaths: [.../* @__PURE__ */ new Set([serverGeneratedDirectory, clientGeneratedDirectory])].map((directory) => path.join(directory, ARTIFACT_PROTOCOL.ignoreFileName)),
691
- serverGeneratedDirectory,
692
- clientGeneratedDirectory,
693
- generatedRegistrationFilePath: path.join(serverGeneratedDirectory, ARTIFACT_PROTOCOL.registrationFileName),
694
- generatedServerContractsFilePath: path.join(serverGeneratedDirectory, ARTIFACT_PROTOCOL.generatedServerContractsFileName)
695
- };
696
- }
697
- /** Resolves every configured path once against the configuration file's directory. */
698
- function resolveVormaConfig(input) {
699
- return Effect.gen(function* () {
700
- const config = yield* Schema.decodeUnknownEffect(VormaConfigSchema)(input.config).pipe(Effect.mapError((cause) => {
701
- return {
702
- _tag: "ConfigResolutionError",
703
- field: "config",
704
- message: `Invalid Vorma configuration: ${cause.message}`,
705
- cause
706
- };
707
- }));
708
- if (config.bodySsr === true) return yield* Effect.fail({
709
- _tag: "ConfigResolutionError",
710
- field: "bodySsr",
711
- message: "bodySsr: true requires server body rendering and hydration, which this runtime does not provide; omit bodySsr or set it to false"
712
- });
713
- const path = yield* Path$1.Path;
714
- if (!path.isAbsolute(input.configFilePath)) return yield* Effect.fail({
715
- _tag: "ConfigResolutionError",
716
- field: "configFilePath",
717
- message: `configFilePath must be absolute, got ${JSON.stringify(input.configFilePath)}`
718
- });
719
- const configDirectory = path.dirname(path.normalize(input.configFilePath));
720
- const serverTsconfigFilePath = path.resolve(configDirectory, config.serverTsconfigFile);
721
- const clientEntryFilePath = path.resolve(configDirectory, config.clientEntry);
722
- const clientTsconfigFilePath = path.resolve(configDirectory, config.clientTsconfigFile);
723
- return {
724
- configFilePath: path.normalize(input.configFilePath),
725
- configDirectory,
726
- serverTsconfigFilePath,
727
- serverProjectDirectory: path.dirname(serverTsconfigFilePath),
728
- clientEntryFilePath,
729
- clientTsconfigFilePath,
730
- publicAssetsSourceDirectory: path.resolve(configDirectory, config.publicAssetsDir),
731
- viteConfigFilePath: path.resolve(configDirectory, config.viteConfigFile),
732
- ...config.criticalCssEntry === void 0 ? {} : { criticalCssEntryFilePath: path.resolve(configDirectory, config.criticalCssEntry) },
733
- publicAssetsBasePath: yield* Effect.fromResult(parsePublicAssetsBasePath(config.publicAssetsBasePath)),
734
- bodySsr: config.bodySsr,
735
- dev: config.dev,
736
- ...getVormaOwnedPaths({
737
- path,
738
- serverProjectDirectory: path.dirname(serverTsconfigFilePath),
739
- clientProjectDirectory: path.dirname(clientTsconfigFilePath)
740
- })
741
- };
742
- });
743
- }
744
- function parsePublicAssetsBasePath(value) {
745
- const parsed = parseUrlPathname(value);
746
- if (Result.isFailure(parsed)) return Result.fail(makePublicAssetsBasePathError(parsed.failure));
747
- if (!matchesPublicAssetsBasePath(parsed.success)) return Result.fail(makePublicAssetsBasePathError());
748
- return Result.succeed(parsed.success);
749
- }
750
- /** The root, or an absolute path with a trailing slash. */
751
- function matchesPublicAssetsBasePath(value) {
752
- return value === "/" || value.startsWith("/") && value.endsWith("/");
753
- }
754
- function makePublicAssetsBasePathError(cause) {
755
- return {
756
- _tag: "ConfigResolutionError",
757
- field: "publicAssetsBasePath",
758
- message: "publicAssetsBasePath must be a normalized absolute URL path ending in \"/\" without an authority, query, or fragment",
759
- ...cause === void 0 ? {} : { cause }
760
- };
761
- }
762
- //#endregion
763
- //#region src/build/typescript-projects.ts
764
- /** Reads native project membership without loading programs or declaration types. */
765
- function readTypescriptProjectRoots(input) {
766
- return withTypescriptApi({
767
- configDirectory: input.configDirectory,
768
- useFn: (api) => {
769
- return runTypescriptOperation(async () => {
770
- return {
771
- server: (await api.parseConfigFile(input.serverTsconfigFilePath)).fileNames,
772
- client: (await api.parseConfigFile(input.clientTsconfigFilePath)).fileNames
773
- };
774
- });
775
- }
776
- });
777
- }
778
- /** Checks actual compiler membership after generation, including restrictive files/include/exclude settings. */
779
- function checkGeneratedTypeInclusion(input) {
780
- const serverFiles = input.serverFiles.filter((file) => !input.knownRootFiles?.server.has(normalizePath(file)));
781
- const clientFiles = input.clientFiles.filter((file) => !input.knownRootFiles?.client.has(normalizePath(file)));
782
- if (serverFiles.length === 0 && clientFiles.length === 0) return Effect.void;
783
- return withTypescriptProjects({
784
- ...input,
785
- useFn: (projects) => {
786
- return Effect.gen(function* () {
787
- const missing = yield* runTypescriptOperation(async () => {
788
- const missing = [];
789
- for (const [project, files, config] of [[
790
- projects.server,
791
- serverFiles,
792
- input.serverTsconfigFilePath
793
- ], [
794
- projects.client,
795
- clientFiles,
796
- input.clientTsconfigFilePath
797
- ]]) for (const file of files) if (await project.program.getSourceFileMetadata(file) === void 0) missing.push({
798
- project: config,
799
- file
800
- });
801
- return missing;
802
- });
803
- return yield* missing.length === 0 ? Effect.void : Effect.fail({
804
- _tag: "TypescriptProjectError",
805
- message: missing.map(({ project, file }) => `${project} does not include generated type file ${file}. Include the _vorma directory beside that tsconfig in its TypeScript project.`).join("\n")
806
- });
807
- });
808
- }
809
- });
810
- }
811
- /** Opens the two configured TypeScript projects for the duration of `useFn`, then closes the checker session. */
812
- function withTypescriptProjects(input) {
813
- return withTypescriptApi({
814
- ...input,
815
- useFn: (api) => {
816
- return Effect.scoped(Effect.gen(function* () {
817
- const projects = findProjects({
818
- snapshot: yield* Effect.acquireRelease(runTypescriptOperation(() => {
819
- return api.updateSnapshot({ openProjects: [input.serverTsconfigFilePath, input.clientTsconfigFilePath] });
820
- }), (snapshot) => Effect.promise(() => snapshot.dispose())),
821
- ...input
822
- });
823
- if (Result.isFailure(projects)) return yield* Effect.fail(projects.failure);
824
- const errors = (yield* runTypescriptOperation(() => {
825
- return Promise.all([projects.success.server.program.getConfigFileParsingDiagnostics(), projects.success.client.program.getConfigFileParsingDiagnostics()]);
826
- })).flat().filter((diagnostic) => {
827
- return diagnostic.category === DiagnosticCategory.Error;
828
- });
829
- if (errors.length > 0) return yield* Effect.fail({
830
- _tag: "TypescriptProjectError",
831
- message: errors.map((error) => `${error.fileName}: TS${error.code}: ${error.text}`).join("\n")
832
- });
833
- return yield* input.useFn(projects.success);
834
- }));
835
- }
836
- });
837
- }
838
- /** Native compiler operations have no cancellation API; settle each operation before its session can close. */
839
- function runTypescriptOperation(fn) {
840
- return Effect.tryPromise({
841
- try: fn,
842
- catch: (cause) => {
843
- return {
844
- _tag: "TypescriptProjectError",
845
- message: "Vorma could not read the application's TypeScript projects",
846
- cause
847
- };
848
- }
849
- }).pipe(Effect.uninterruptible);
850
- }
851
- function withTypescriptApi(input) {
852
- return Effect.scoped(Effect.gen(function* () {
853
- const api = yield* Effect.acquireRelease(runTypescriptOperation(async () => {
854
- return new API({
855
- cwd: input.configDirectory,
856
- ...input.fileSystem === void 0 ? {} : { fs: input.fileSystem }
857
- });
858
- }), (api) => Effect.promise(() => api.close()));
859
- return yield* input.useFn(api);
860
- }));
861
- }
862
- function findProjects(input) {
863
- const server = input.snapshot.getProject(input.serverTsconfigFilePath);
864
- const client = input.snapshot.getProject(input.clientTsconfigFilePath);
865
- if (server === void 0 || client === void 0) return Result.fail({
866
- _tag: "TypescriptProjectError",
867
- message: `TypeScript did not load ${server === void 0 ? input.serverTsconfigFilePath : input.clientTsconfigFilePath}`
868
- });
869
- return Result.succeed({
870
- server,
871
- client
872
- });
873
- }
874
- //#endregion
875
- //#region src/build/inspect-projects.ts
876
- /** Reads every registerable declaration, view, the client application, and every public URL call from the two projects. */
877
- function inspectProjects(input) {
878
- return Effect.gen(function* () {
879
- const path = yield* Path$1.Path;
880
- const excludedDirectories = getGeneratedSourceDirectories(input.layout);
881
- const serverFiles = yield* runTypescriptOperation(() => {
882
- return listOwnSourceFiles({
883
- project: input.projects.server,
884
- excludedDirectories,
885
- path
886
- });
887
- });
888
- const clientFiles = yield* runTypescriptOperation(() => {
889
- return listOwnSourceFiles({
890
- project: input.projects.client,
891
- excludedDirectories,
892
- path
893
- });
894
- });
895
- const serverRenderer = yield* runTypescriptOperation(() => {
896
- return createTypeRenderer({
897
- checker: input.projects.server.checker,
898
- program: input.projects.server.program,
899
- emitter: input.projects.server.emitter
900
- });
901
- });
902
- const serverDeclarations = yield* Effect.fromResult(yield* runTypescriptOperation(() => {
903
- return inspectServerDeclarations({
904
- checker: input.projects.server.checker,
905
- program: input.projects.server.program,
906
- files: serverFiles,
907
- renderer: serverRenderer
908
- });
909
- }));
910
- const client = yield* Effect.fromResult(yield* runTypescriptOperation(() => {
911
- return inspectClientProject({
912
- checker: input.projects.client.checker,
913
- files: clientFiles,
914
- clientEntryFilePath: input.layout.clientEntryFilePath
915
- });
916
- }));
917
- return {
918
- serverDeclarations,
919
- clientViews: client.views,
920
- clientApplication: client.application,
921
- publicUrlSites: client.publicUrlSites,
922
- clientSourceTexts: client.sourceTexts,
923
- typeAliases: serverRenderer.renderAliases(),
924
- effectModules: serverRenderer.getEffectModules(),
925
- transportTypeNames: serverRenderer.getTransportTypeNames()
926
- };
927
- });
928
- }
929
- /** The project's own modules: not a library, not a declaration file, and not under a Vorma output directory. */
930
- async function listOwnSourceFiles(input) {
931
- const { program } = input.project;
932
- const files = [];
933
- for (const fileName of await program.getSourceFileNames()) {
934
- if (fileName.includes("/node_modules/") || input.excludedDirectories.some((directory) => {
935
- const relativePath = input.path.relative(directory, fileName);
936
- return containsRelativePath({
937
- relativePath,
938
- separator: input.path.sep,
939
- absolute: input.path.isAbsolute(relativePath)
940
- });
941
- })) continue;
942
- const metadata = await program.getSourceFileMetadata(fileName);
943
- if (metadata?.isDefaultLibrary || metadata?.isFromExternalLibrary) continue;
944
- const sourceFile = await program.getSourceFile(fileName);
945
- if (sourceFile === void 0 || sourceFile.isDeclarationFile) continue;
946
- files.push(sourceFile);
947
- }
948
- return files.sort((left, right) => {
949
- return compareSourceText({
950
- left: left.fileName,
951
- right: right.fileName
952
- });
953
- });
954
- }
955
- /** Export bindings retain their public names while aliases resolve to one declaration identity. */
956
- async function listModuleExports(input) {
957
- const { checker, sourceFile } = input;
958
- const moduleSymbol = await checker.getSymbolAtLocation(sourceFile);
959
- if (moduleSymbol === void 0) return [];
960
- const exports = [];
961
- for (const exported of await checker.getExportsOfModule(moduleSymbol)) {
962
- const symbol = (exported.flags & SymbolFlags.Alias) !== 0 ? await checker.getAliasedSymbol(exported) : exported;
963
- if (await checker.isUnknownSymbol(symbol)) continue;
964
- const type = await checker.getTypeOfSymbol(symbol);
965
- if (type === void 0) continue;
966
- exports.push({
967
- exportName: exported.name,
968
- symbol,
969
- type
970
- });
971
- }
972
- return exports;
973
- }
974
- /** Prefer a declaration's own exported module so a re-export barrel does not enlarge a view's import closure. */
975
- async function listRegistrationExports(input) {
976
- const exports = /* @__PURE__ */ new Map();
977
- for (const sourceFile of input.files) for (const moduleExport of await listModuleExports({
978
- checker: input.checker,
979
- sourceFile
980
- })) {
981
- const own = moduleExport.symbol.declarations.some((declaration) => {
982
- return String(declaration.path) === String(sourceFile.path);
983
- });
984
- if (!exports.has(moduleExport.symbol.id) || own) exports.set(moduleExport.symbol.id, {
985
- sourceFile,
986
- moduleExport
987
- });
988
- }
989
- return [...exports.values()];
990
- }
991
- /** The phantom property that carries a definition's static types, by the unique symbol that keys it. */
992
- async function findPhantomProperty(input) {
993
- const prefix = `__@${input.symbolName}@`;
994
- return (await input.checker.getPropertiesOfType(input.type)).find((property) => String(property.escapedName).startsWith(prefix));
995
- }
996
- async function readPropertyType(input) {
997
- const property = await input.checker.getPropertyOfType(input.type, input.name);
998
- return property === void 0 ? void 0 : await input.checker.getTypeOfSymbol(property);
999
- }
1000
- async function readLiteralProperty(input) {
1001
- const propertyType = await readPropertyType(input);
1002
- if (propertyType === void 0) return Result.fail(`the declaration has no ${input.name} property`);
1003
- if (!propertyType.isStringLiteralType()) return Result.fail(`${input.name} must have a literal type; its type is ${await input.checker.typeToString(propertyType)}`);
1004
- return Result.succeed(propertyType.value);
1005
- }
1006
- function matchesUndefinedType(type) {
1007
- return type.isIntrinsicType() && type.intrinsicName === "undefined";
1008
- }
1009
- async function inspectServerDeclarations(input) {
1010
- const { checker, program, renderer } = input;
1011
- const declarations = [];
1012
- const candidates = [];
1013
- for (const { sourceFile, moduleExport } of await listRegistrationExports(input)) {
1014
- const source = {
1015
- filePath: sourceFile.fileName,
1016
- exportName: moduleExport.exportName
1017
- };
1018
- const phantom = await findPhantomProperty({
1019
- checker,
1020
- type: moduleExport.type,
1021
- symbolName: "DEFINITION_TYPES"
1022
- });
1023
- if (phantom === void 0) continue;
1024
- const tag = await readPropertyType({
1025
- checker,
1026
- type: moduleExport.type,
1027
- name: "_tag"
1028
- });
1029
- if (tag === void 0 || !tag.isStringLiteralType()) continue;
1030
- if (tag.value !== "UiLoader" && tag.value !== "Endpoint") continue;
1031
- const phantomType = await checker.getTypeOfSymbol(phantom);
1032
- const types = phantomType === void 0 ? void 0 : await checker.getNonNullableType(phantomType);
1033
- if (types === void 0) return Result.fail(makeIssue(source, "the definition carries no static types"));
1034
- const pattern = await readLiteralProperty({
1035
- checker,
1036
- type: moduleExport.type,
1037
- name: "pattern"
1038
- });
1039
- if (Result.isFailure(pattern)) return Result.fail(makeIssue(source, pattern.failure));
1040
- const method = await readPropertyType({
1041
- checker,
1042
- type: moduleExport.type,
1043
- name: "method"
1044
- });
1045
- candidates.push({
1046
- source,
1047
- kind: tag.value,
1048
- type: moduleExport.type,
1049
- types,
1050
- key: JSON.stringify([
1051
- tag.value,
1052
- pattern.success,
1053
- method?.isStringLiteralType() === true ? method.value : ""
1054
- ])
1055
- });
1056
- }
1057
- candidates.sort((left, right) => {
1058
- return compareSourceText({
1059
- left: left.key,
1060
- right: right.key
1061
- });
1062
- });
1063
- for (const { source, kind, type, types } of candidates) {
1064
- const inspected = kind === "UiLoader" ? await inspectUiLoader({
1065
- checker,
1066
- renderer,
1067
- source,
1068
- type,
1069
- types
1070
- }) : await inspectEndpoint({
1071
- checker,
1072
- program,
1073
- renderer,
1074
- source,
1075
- type,
1076
- types
1077
- });
1078
- if (Result.isFailure(inspected)) return Result.fail(inspected.failure);
1079
- declarations.push(inspected.success);
1080
- }
1081
- return Result.succeed(declarations);
1082
- }
1083
- /** An issue at a declaration; the message names the file and export so a report is complete on its own. */
1084
- function makeIssue(source, message) {
1085
- return {
1086
- _tag: "InspectionIssue",
1087
- ...source,
1088
- message: `${source.filePath} (export ${source.exportName}): ${message}`
1089
- };
1090
- }
1091
- async function renderContract(input) {
1092
- return Result.mapError(await input.renderer.renderType({
1093
- type: input.type,
1094
- depth: 2
1095
- }), (issue) => {
1096
- return makeIssue(input.source, `the ${input.field} contract cannot cross the wire: ${issue.reason} (${issue.typeText})`);
1097
- });
1098
- }
1099
- /** A contract's text, or `undefined` for the absent contract that the type `undefined` denotes. */
1100
- async function renderOptionalContract(input) {
1101
- const type = await readPropertyType({
1102
- checker: input.checker,
1103
- type: input.types,
1104
- name: input.field
1105
- });
1106
- if (type === void 0 || matchesUndefinedType(type)) return Result.succeed(void 0);
1107
- return await renderContract({
1108
- ...input,
1109
- type
1110
- });
1111
- }
1112
- async function inspectUiLoader(input) {
1113
- const { checker, renderer, source } = input;
1114
- const pattern = await readLiteralProperty({
1115
- checker,
1116
- type: input.type,
1117
- name: "pattern"
1118
- });
1119
- if (Result.isFailure(pattern)) return Result.fail(makeIssue(source, pattern.failure));
1120
- const loaderInput = await renderOptionalContract({
1121
- checker,
1122
- renderer,
1123
- source,
1124
- field: "input",
1125
- types: input.types
1126
- });
1127
- if (Result.isFailure(loaderInput)) return Result.fail(loaderInput.failure);
1128
- const outputType = await readPropertyType({
1129
- checker,
1130
- type: input.types,
1131
- name: "output"
1132
- });
1133
- if (outputType === void 0) return Result.fail(makeIssue(source, "a UI loader must have an output contract"));
1134
- const output = await renderContract({
1135
- renderer,
1136
- source,
1137
- field: "output",
1138
- type: outputType
1139
- });
1140
- if (Result.isFailure(output)) return Result.fail(output.failure);
1141
- return Result.succeed({
1142
- ...source,
1143
- kind: "UiLoader",
1144
- pattern: pattern.success,
1145
- input: loaderInput.success,
1146
- output: output.success
1147
- });
1148
- }
1149
- async function inspectEndpoint(input) {
1150
- const { checker, program, renderer, source } = input;
1151
- const literals = {
1152
- kind: "",
1153
- method: "",
1154
- pattern: ""
1155
- };
1156
- for (const name of [
1157
- "kind",
1158
- "method",
1159
- "pattern"
1160
- ]) {
1161
- const literal = await readLiteralProperty({
1162
- checker,
1163
- type: input.type,
1164
- name
1165
- });
1166
- if (Result.isFailure(literal)) return Result.fail(makeIssue(source, literal.failure));
1167
- literals[name] = literal.success;
1168
- }
1169
- const operation = literals.kind;
1170
- if (operation !== "query" && operation !== "mutation") return Result.fail(makeIssue(source, `unknown endpoint kind ${operation}`));
1171
- const method = literals.method;
1172
- if (!matchesHttpMethod(method)) return Result.fail(makeIssue(source, `unknown endpoint method ${method}`));
1173
- const inputType = await readPropertyType({
1174
- checker,
1175
- type: input.types,
1176
- name: "input"
1177
- });
1178
- const hasInput = inputType !== void 0 && !matchesUndefinedType(inputType);
1179
- const inputTransport = !hasInput ? "none" : method === "GET" ? "search" : await matchesFormInput({
1180
- checker,
1181
- program,
1182
- types: input.types
1183
- }) ? "formData" : "json";
1184
- const endpointInput = inputType === void 0 || !hasInput || inputTransport === "formData" ? Result.succeed(void 0) : await renderContract({
1185
- renderer,
1186
- source,
1187
- field: "input",
1188
- type: inputType
1189
- });
1190
- if (Result.isFailure(endpointInput)) return Result.fail(endpointInput.failure);
1191
- const outputType = await readPropertyType({
1192
- checker,
1193
- type: input.types,
1194
- name: "output"
1195
- });
1196
- if (outputType === void 0) return Result.fail(makeIssue(source, "an endpoint must have an output contract"));
1197
- const output = await renderContract({
1198
- renderer,
1199
- source,
1200
- field: "output",
1201
- type: outputType
1202
- });
1203
- if (Result.isFailure(output)) return Result.fail(output.failure);
1204
- const errors = await inspectEndpointErrors({
1205
- checker,
1206
- renderer,
1207
- source,
1208
- types: input.types
1209
- });
1210
- if (Result.isFailure(errors)) return Result.fail(errors.failure);
1211
- return Result.succeed({
1212
- ...source,
1213
- kind: "Endpoint",
1214
- operation,
1215
- method,
1216
- pattern: literals.pattern,
1217
- input: endpointInput.success,
1218
- inputTransport,
1219
- output: output.success,
1220
- errors: errors.success
1221
- });
1222
- }
1223
- /**
1224
- * Whether an endpoint's input arrives as a form: its schema encodes to the
1225
- * form-transport marker carried by Vorma's FormData schema and its derived
1226
- * codecs. The runtime recognizes the same factory's schema annotation.
1227
- */
1228
- async function matchesFormInput(input) {
1229
- const symbol = await (await readPropertyType({
1230
- checker: input.checker,
1231
- type: input.types,
1232
- name: "inputEncoded"
1233
- }))?.getSymbol();
1234
- return symbol !== void 0 && symbol.name === "FormDataEncoded" && symbol.declarations.some((declaration) => {
1235
- return /\/vorma\/(?:src|dist)\/server\/form-data\.(?:d\.)?ts$/.test(String(declaration.path));
1236
- }) && await matchesLibrarySymbol({
1237
- program: input.program,
1238
- symbol
1239
- });
1240
- }
1241
- /** Each declared error: its decoded `_tag` and transportable schema fields. */
1242
- async function inspectEndpointErrors(input) {
1243
- const { checker, renderer, source } = input;
1244
- const errorsType = await readPropertyType({
1245
- checker,
1246
- type: input.types,
1247
- name: "errors"
1248
- });
1249
- if (errorsType === void 0) return Result.succeed([]);
1250
- const numberType = await checker.getNumberType();
1251
- const elementType = (await checker.getIndexInfosOfType(errorsType)).find((index) => {
1252
- return index.keyType.id === numberType.id;
1253
- })?.valueType;
1254
- if (elementType === void 0) return Result.fail(makeIssue(source, "declared errors must have an array element type"));
1255
- const errors = [];
1256
- const variantsByTag = [];
1257
- const elements = elementType.isUnionType() ? await elementType.getTypes() : [elementType];
1258
- for (const element of elements) {
1259
- if (element.isIntrinsicType() && element.intrinsicName === "never") continue;
1260
- const schemaType = await readPropertyType({
1261
- checker,
1262
- type: element,
1263
- name: "schema"
1264
- });
1265
- const decodedType = schemaType === void 0 ? void 0 : await readPropertyType({
1266
- checker,
1267
- type: schemaType,
1268
- name: "Type"
1269
- });
1270
- if (decodedType === void 0) return Result.fail(makeIssue(source, "each declared error's schema must expose its decoded Type"));
1271
- const variants = decodedType.isUnionType() ? await decodedType.getTypes() : [decodedType];
1272
- for (const variant of variants) {
1273
- const tag = await readLiteralProperty({
1274
- checker,
1275
- type: variant,
1276
- name: "_tag"
1277
- });
1278
- if (Result.isFailure(tag)) return Result.fail(makeIssue(source, `a declared error needs a literal _tag: ${tag.failure}`));
1279
- variantsByTag.push({
1280
- tag: tag.success,
1281
- type: variant
1282
- });
1283
- }
1284
- }
1285
- variantsByTag.sort((left, right) => {
1286
- return compareSourceText({
1287
- left: left.tag,
1288
- right: right.tag
1289
- });
1290
- });
1291
- for (const variant of variantsByTag) {
1292
- const fields = await renderContract({
1293
- renderer,
1294
- source,
1295
- field: `error ${variant.tag}`,
1296
- type: variant.type
1297
- });
1298
- if (Result.isFailure(fields)) return Result.fail(fields.failure);
1299
- errors.push({
1300
- tag: variant.tag,
1301
- fields: fields.success
1302
- });
1303
- }
1304
- return Result.succeed(errors);
1305
- }
1306
- async function inspectClientProject(input) {
1307
- const { checker } = input;
1308
- const views = [];
1309
- const applications = /* @__PURE__ */ new Map();
1310
- const publicUrlSites = [];
1311
- const sourceTexts = /* @__PURE__ */ new Map();
1312
- for (const { sourceFile, moduleExport } of await listRegistrationExports(input)) {
1313
- const source = {
1314
- filePath: sourceFile.fileName,
1315
- exportName: moduleExport.exportName
1316
- };
1317
- if (await findPhantomProperty({
1318
- checker,
1319
- type: moduleExport.type,
1320
- symbolName: "UI_VIEW_TYPES"
1321
- }) !== void 0) {
1322
- const pattern = await readLiteralProperty({
1323
- checker,
1324
- type: moduleExport.type,
1325
- name: "pattern"
1326
- });
1327
- if (Result.isFailure(pattern)) return Result.fail(makeIssue(source, pattern.failure));
1328
- views.push({
1329
- ...source,
1330
- pattern: pattern.success
1331
- });
1332
- }
1333
- }
1334
- for (const sourceFile of input.files) {
1335
- if (sourceFile.fileName === input.clientEntryFilePath) {
1336
- for (const moduleExport of await listModuleExports({
1337
- checker,
1338
- sourceFile
1339
- })) if (await findPhantomProperty({
1340
- checker,
1341
- type: moduleExport.type,
1342
- symbolName: "CLIENT_APPLICATION_TYPES"
1343
- }) !== void 0) applications.set(moduleExport.symbol.id, {
1344
- filePath: sourceFile.fileName,
1345
- exportName: moduleExport.exportName
1346
- });
1347
- }
1348
- const sites = await collectPublicUrlSites({
1349
- checker,
1350
- sourceFile
1351
- });
1352
- if (Result.isFailure(sites)) return Result.fail(sites.failure);
1353
- if (sites.success.length > 0) {
1354
- publicUrlSites.push(...sites.success);
1355
- sourceTexts.set(sourceFile.fileName, sourceFile.text);
1356
- }
1357
- }
1358
- const [application, ...moreApplications] = applications.values();
1359
- if (application === void 0 || moreApplications.length > 0) return Result.fail({
1360
- _tag: "InspectionIssue",
1361
- filePath: input.clientEntryFilePath,
1362
- message: `${input.clientEntryFilePath}: the client entry must export exactly one ClientApplication.make value, which the generated client declaration reads for the application's service types`
1363
- });
1364
- return Result.succeed({
1365
- views,
1366
- application,
1367
- publicUrlSites,
1368
- sourceTexts
1369
- });
1370
- }
1371
- /** Whether a symbol is the `getPublicUrl` declared by `@vorma/client`, however the module reached it. */
1372
- async function matchesPublicUrlSymbol(input) {
1373
- const symbol = (input.symbol.flags & SymbolFlags.Alias) !== 0 ? await input.checker.getAliasedSymbol(input.symbol) : input.symbol;
1374
- if (symbol.name !== "getPublicUrl") return false;
1375
- return (await symbol.getJsDocTags(input.checker)).some((tag) => tag.name === "vormaPublicAsset");
1376
- }
1377
- /** Every call of `getPublicUrl` in one file. A use that is not a direct call with a string literal cannot be replaced by the build. */
1378
- async function collectPublicUrlSites(input) {
1379
- const { checker, sourceFile } = input;
1380
- const sites = [];
1381
- let issue;
1382
- function report(node, message) {
1383
- if (issue === void 0) {
1384
- const position = node.getStart(sourceFile);
1385
- const { line, character } = sourceFile.getLineAndCharacterOfPosition(position);
1386
- issue = {
1387
- _tag: "InspectionIssue",
1388
- filePath: sourceFile.fileName,
1389
- position,
1390
- message: `${sourceFile.fileName}:${line + 1}:${character + 1}: ${message}`
1391
- };
1392
- }
1393
- }
1394
- async function visit(node) {
1395
- if (issue !== void 0 || isTypeNode(node)) return;
1396
- if (isIdentifier(node) || isPropertyAccessExpression(node) || isElementAccessExpression(node)) {
1397
- const parent = node.parent;
1398
- if (isImportSpecifier(parent) || isImportClause(parent) || isNamespaceImport(parent) || isExportSpecifier(parent)) return;
1399
- const symbol = isElementAccessExpression(node) ? await getElementAccessSymbol({
1400
- checker,
1401
- node
1402
- }) : await checker.getSymbolAtLocation(node);
1403
- if (symbol !== void 0 && await matchesPublicUrlSymbol({
1404
- checker,
1405
- symbol
1406
- })) {
1407
- let callee = node;
1408
- while (matchesTransparentExpression(callee.parent) && callee.parent.expression === callee) callee = callee.parent;
1409
- const call = callee.parent;
1410
- if (!isCallExpression(call) || call.expression !== callee) {
1411
- report(node, "getPublicUrl must be called directly with a string literal so the build can replace the call");
1412
- return;
1413
- }
1414
- const [authoredArgument, ...moreArguments] = call.arguments;
1415
- const argument = authoredArgument === void 0 ? void 0 : unwrapExpression(authoredArgument);
1416
- if (argument === void 0 || moreArguments.length > 0 || !(isStringLiteral(argument) || isNoSubstitutionTemplateLiteral(argument))) {
1417
- report(call, "getPublicUrl requires exactly one string literal argument, so the build can replace the call with the hashed URL");
1418
- return;
1419
- }
1420
- sites.push({
1421
- filePath: sourceFile.fileName,
1422
- start: call.getStart(sourceFile),
1423
- end: call.getEnd(),
1424
- key: argument.text
1425
- });
1426
- return;
1427
- }
1428
- }
1429
- const children = [];
1430
- node.forEachChild((child) => {
1431
- children.push(child);
1432
- });
1433
- for (const child of children) await visit(child);
1434
- }
1435
- await visit(sourceFile);
1436
- return issue === void 0 ? Result.succeed(sites) : Result.fail(issue);
1437
- }
1438
- /** Parentheses and erased TypeScript wrappers preserve the underlying expression. */
1439
- function matchesTransparentExpression(node) {
1440
- return isParenthesizedExpression(node) || isAsExpression(node) || isTypeAssertion(node) || isSatisfiesExpression(node) || isNonNullExpression(node);
1441
- }
1442
- function unwrapExpression(node) {
1443
- while (matchesTransparentExpression(node)) node = node.expression;
1444
- return node;
1445
- }
1446
- async function getElementAccessSymbol(input) {
1447
- const objectType = await input.checker.getTypeAtLocation(input.node.expression);
1448
- const keyType = await input.checker.getTypeAtLocation(input.node.argumentExpression);
1449
- return objectType !== void 0 && keyType?.isStringLiteralType() ? await input.checker.getPropertyOfType(objectType, keyType.value) : void 0;
1450
- }
1451
- //#endregion
1452
- //#region src/build/compile-application.ts
1453
- function compareSources(left, right) {
1454
- const byPath = compareSourceText({
1455
- left: left.filePath,
1456
- right: right.filePath
1457
- });
1458
- return byPath === 0 ? compareSourceText({
1459
- left: left.exportName,
1460
- right: right.exportName
1461
- }) : byPath;
1462
- }
1463
- function getSegmentSources(segment) {
1464
- return [...segment.loader === void 0 ? [] : [segment.loader], ...segment.view === void 0 ? [] : [segment.view]];
1465
- }
1466
- /** Joins the inspected declarations and views into one validated model. */
1467
- function compileApplicationModel(inspection) {
1468
- const issues = [];
1469
- const serverDeclarations = [...inspection.serverDeclarations].sort(compareSources);
1470
- const clientViews = [...inspection.clientViews].sort(compareSources);
1471
- const uiLoaders = serverDeclarations.filter((declaration) => {
1472
- return declaration.kind === "UiLoader";
1473
- });
1474
- const endpoints = serverDeclarations.filter((declaration) => {
1475
- return declaration.kind === "Endpoint";
1476
- });
1477
- const segments = /* @__PURE__ */ new Map();
1478
- const loaderSources = /* @__PURE__ */ new Map();
1479
- for (const loader of uiLoaders) {
1480
- const registered = compileUiPattern(loader.pattern);
1481
- if (Result.isFailure(registered)) {
1482
- issues.push({
1483
- _tag: "InvalidPattern",
1484
- role: "uiLoader",
1485
- pattern: loader.pattern,
1486
- source: loader,
1487
- cause: registered.failure
1488
- });
1489
- continue;
1490
- }
1491
- const key = registered.success.pattern;
1492
- const sources = loaderSources.get(key);
1493
- if (sources === void 0) {
1494
- loaderSources.set(key, [loader]);
1495
- segments.set(key, {
1496
- pattern: loader.pattern,
1497
- registered: registered.success,
1498
- loader,
1499
- view: void 0
1500
- });
1501
- } else sources.push(loader);
1502
- }
1503
- const viewSources = /* @__PURE__ */ new Map();
1504
- for (const view of clientViews) {
1505
- const registered = compileUiPattern(view.pattern);
1506
- if (Result.isFailure(registered)) {
1507
- issues.push({
1508
- _tag: "InvalidPattern",
1509
- role: "clientView",
1510
- pattern: view.pattern,
1511
- source: view,
1512
- cause: registered.failure
1513
- });
1514
- continue;
1515
- }
1516
- const key = registered.success.pattern;
1517
- const sources = viewSources.get(key);
1518
- if (sources !== void 0) {
1519
- sources.push(view);
1520
- continue;
1521
- }
1522
- viewSources.set(key, [view]);
1523
- const existing = segments.get(key);
1524
- segments.set(key, {
1525
- pattern: existing === void 0 ? view.pattern : existing.pattern,
1526
- registered: registered.success,
1527
- loader: existing === void 0 ? void 0 : existing.loader,
1528
- view
1529
- });
1530
- }
1531
- for (const [pattern, sources] of viewSources) if (sources.length > 1) issues.push({
1532
- _tag: "DuplicateClientView",
1533
- pattern,
1534
- sources
1535
- });
1536
- const endpointSources = /* @__PURE__ */ new Map();
1537
- for (const endpoint of endpoints) {
1538
- const registered = compileEndpointPattern(endpoint.pattern);
1539
- if (Result.isFailure(registered)) {
1540
- issues.push({
1541
- _tag: "InvalidPattern",
1542
- role: "endpoint",
1543
- pattern: endpoint.pattern,
1544
- source: endpoint,
1545
- cause: registered.failure
1546
- });
1547
- continue;
1548
- }
1549
- const key = `${endpoint.method} ${registered.success.pattern}`;
1550
- const sources = endpointSources.get(key);
1551
- if (sources === void 0) endpointSources.set(key, [endpoint]);
1552
- else sources.push(endpoint);
1553
- }
1554
- const orderedSegments = [...segments.values()].sort((left, right) => {
1555
- return compareSourceText({
1556
- left: left.pattern,
1557
- right: right.pattern
1558
- });
1559
- });
1560
- for (const conflict of findConflictingDynamicParameterNames({
1561
- patterns: orderedSegments,
1562
- getMatcherPattern: (segment) => segment.registered.pattern,
1563
- getSegments: (segment) => segment.registered.segments,
1564
- makeCause: ({ conflict, attemptedPattern }) => {
1565
- return {
1566
- _tag: "ConflictingDynamicParameterName",
1567
- ...conflict,
1568
- pattern: attemptedPattern.pattern
1569
- };
1570
- }
1571
- })) issues.push({
1572
- _tag: "ConflictingUiPatternParameters",
1573
- patterns: conflict.patterns.map((segment) => segment.pattern),
1574
- sources: conflict.patterns.flatMap(getSegmentSources).sort(compareSources),
1575
- cause: conflict.cause
1576
- });
1577
- if (issues.length > 0) return Result.fail(issues);
1578
- const linkedSegments = createNestedRouteChains({ nodes: orderedSegments }).map(({ target, chain }) => {
1579
- return {
1580
- ...target,
1581
- searchOwnerInputs: chain.flatMap((node) => {
1582
- return node.loader === void 0 || node.loader.input === void 0 ? [] : [node.loader.input];
1583
- })
1584
- };
1585
- });
1586
- return Result.succeed({
1587
- serverDeclarations,
1588
- uiLoaders: [...loaderSources.values()].flatMap((sources) => sources.slice(0, 1)),
1589
- endpoints: [...endpointSources.values()].flatMap((sources) => sources.slice(0, 1)),
1590
- clientViews: [...viewSources.values()].flatMap((sources) => sources.slice(0, 1)),
1591
- clientViewModules: [...new Set(clientViews.map((view) => view.filePath))].sort((left, right) => compareSourceText({
1592
- left,
1593
- right
1594
- })),
1595
- clientApplication: inspection.clientApplication,
1596
- segments: linkedSegments
1597
- });
1598
- }
1599
- //#endregion
1600
- //#region src/build/output-files.ts
1601
- /** Atomically replaces generated source that TypeScript and build tools can read. */
1602
- function writeOutputFile(input) {
1603
- return Effect.gen(function* () {
1604
- const fileSystem = yield* FileSystem$1.FileSystem;
1605
- const path = yield* Path$1.Path;
1606
- const directory = path.dirname(input.filePath);
1607
- yield* fileSystem.makeDirectory(directory, { recursive: true });
1608
- const bytes = typeof input.source === "string" ? new TextEncoder().encode(input.source) : input.source;
1609
- yield* Effect.acquireUseRelease(fileSystem.makeTempDirectory({
1610
- directory,
1611
- prefix: "_vorma-write-"
1612
- }), (tempDirectory) => {
1613
- return Effect.gen(function* () {
1614
- const temporary = path.join(tempDirectory, "output");
1615
- yield* fileSystem.writeFile(temporary, bytes);
1616
- yield* fileSystem.rename(temporary, input.filePath);
1617
- });
1618
- }, (tempDirectory) => {
1619
- return fileSystem.remove(tempDirectory, {
1620
- force: true,
1621
- recursive: true
1622
- }).pipe(Effect.catch((cause) => {
1623
- return Effect.logWarning("Could not remove the temporary output directory " + tempDirectory, cause);
1624
- }));
1625
- });
1626
- }).pipe(Effect.uninterruptible);
1627
- }
1628
- //#endregion
1629
- //#region src/build/public-assets.ts
1630
- /** Hashes and publishes each public file's exact bytes, retaining only metadata and erased key types. */
1631
- function collectPublicAssets(config) {
1632
- return Effect.gen(function* () {
1633
- const fileSystem = yield* FileSystem$1.FileSystem;
1634
- const path = yield* Path$1.Path;
1635
- const root = config.publicAssetsSourceDirectory;
1636
- const pending = [{
1637
- directory: root,
1638
- ancestors: /* @__PURE__ */ new Set([root])
1639
- }];
1640
- const files = [];
1641
- while (pending.length > 0) {
1642
- const directories = pending.splice(0);
1643
- const inspections = yield* Effect.forEach(directories, ({ directory, ancestors }) => {
1644
- return Effect.gen(function* () {
1645
- const names = yield* atAssetPath({
1646
- sourcePath: directory,
1647
- message: "Cannot list public assets",
1648
- effect: fileSystem.readDirectory(directory)
1649
- });
1650
- return {
1651
- entries: yield* Effect.forEach(names, (name) => {
1652
- const sourcePath = path.join(directory, name);
1653
- return atAssetPath({
1654
- sourcePath,
1655
- message: "Cannot inspect public asset",
1656
- effect: Effect.gen(function* () {
1657
- const canonicalPath = yield* fileSystem.realPath(sourcePath);
1658
- const stat = yield* fileSystem.stat(canonicalPath);
1659
- return {
1660
- sourcePath,
1661
- canonicalPath,
1662
- type: stat.type
1663
- };
1664
- })
1665
- });
1666
- }, { concurrency: "unbounded" }),
1667
- ancestors
1668
- };
1669
- });
1670
- }, { concurrency: "unbounded" });
1671
- for (const { entries, ancestors } of inspections) for (const entry of entries) if (entry.type === "File") files.push(entry);
1672
- else if (entry.type === "Directory") {
1673
- if (ancestors.has(entry.canonicalPath)) return yield* Effect.fail({
1674
- _tag: "PublicAssetIssue",
1675
- sourcePath: entry.sourcePath,
1676
- message: "Public asset directories must not contain symbolic-link cycles"
1677
- });
1678
- pending.push({
1679
- directory: entry.sourcePath,
1680
- ancestors: /* @__PURE__ */ new Set([...ancestors, entry.canonicalPath])
1681
- });
1682
- } else return yield* Effect.fail({
1683
- _tag: "PublicAssetIssue",
1684
- sourcePath: entry.sourcePath,
1685
- message: `Public assets must be regular files or directories; found ${entry.type}`
1686
- });
1687
- }
1688
- files.sort((left, right) => {
1689
- return compareSourceText({
1690
- left: left.sourcePath,
1691
- right: right.sourcePath
1692
- });
1693
- });
1694
- const assets = yield* Effect.forEach(files, ({ sourcePath, canonicalPath }) => {
1695
- return atAssetPath({
1696
- sourcePath,
1697
- message: "Cannot publish public asset",
1698
- effect: Effect.gen(function* () {
1699
- const source = yield* fileSystem.readFile(canonicalPath);
1700
- const digest = blake3(source);
1701
- const sourceKey = toPosixRelativePath({
1702
- path,
1703
- from: root,
1704
- to: sourcePath
1705
- });
1706
- const pathBytes = new TextEncoder().encode(sourceKey);
1707
- const identity = new Uint8Array(pathBytes.length + 1 + 32);
1708
- identity.set(pathBytes);
1709
- identity.set(digest, pathBytes.length + 1);
1710
- const suffix = toContentDigest(identity).slice(0, 12);
1711
- const basename = path.basename(sourceKey);
1712
- const dot = basename.lastIndexOf(".");
1713
- const extension = dot < 0 ? "" : basename.slice(dot);
1714
- const stem = sourceKey.slice(0, sourceKey.length - extension.length).replaceAll("/", "_");
1715
- const fileName = `${ARTIFACT_PROTOCOL.publicOutputFileNamePrefix}${stem}_${suffix}${extension}`.replaceAll(/[<>:"/\\|?*\u0000-\u001f]/g, "_").replace(/[. ]+$/, (tail) => "_".repeat(tail.length));
1716
- const url = `${config.publicAssetsBasePath}${encodeURIComponent(fileName)}`;
1717
- yield* fileSystem.writeFile(path.join(config.publicDirectory, fileName), source);
1718
- return {
1719
- sourceKey,
1720
- sourcePath,
1721
- digest: encodeContentDigest(digest),
1722
- url,
1723
- fileName
1724
- };
1725
- })
1726
- });
1727
- }, { concurrency: 16 });
1728
- return {
1729
- assets,
1730
- publicFilemap: Object.fromEntries(assets.map((asset) => [asset.sourceKey, asset.url])),
1731
- publicFilepaths: [...new Set(assets.map((asset) => asset.url))],
1732
- types: {
1733
- filePath: path.join(config.clientGeneratedDirectory, ARTIFACT_PROTOCOL.generatedAssetKeysFileName),
1734
- source: [
1735
- "// Code generated by Vorma. DO NOT EDIT.",
1736
- "",
1737
- "import \"@vorma/client\";",
1738
- "",
1739
- renderAssetKeyDeclaration({
1740
- assets,
1741
- depth: 0,
1742
- prefix: "export type PublicAssetKey ="
1743
- }),
1744
- "",
1745
- "declare module \"@vorma/client\" {",
1746
- " interface ClientApplicationContracts {",
1747
- " readonly assets: PublicAssetKey;",
1748
- " }",
1749
- "}",
1750
- ""
1751
- ].join("\n")
1752
- }
1753
- };
1754
- });
1755
- }
1756
- function renderAssetKeyDeclaration(input) {
1757
- const [only, ...more] = input.assets;
1758
- if (only === void 0) return `${input.prefix} never;`;
1759
- if (more.length === 0) return `${input.prefix} ${JSON.stringify(only.sourceKey)};`;
1760
- return input.prefix + "\n" + input.assets.map((asset) => `${" ".repeat(input.depth + 1)}| ${JSON.stringify(asset.sourceKey)}`).join("\n") + ";";
1761
- }
1762
- function atAssetPath(input) {
1763
- return input.effect.pipe(Effect.mapError((cause) => {
1764
- return {
1765
- _tag: "PublicAssetIssue",
1766
- sourcePath: input.sourcePath,
1767
- message: input.message,
1768
- cause
1769
- };
1770
- }));
1771
- }
1772
- //#endregion
1773
- //#region src/build/render-server-generated.ts
1774
- /** One namespace import per source file that exports a registered declaration, in a stable order. */
1775
- function createModuleNamespaces(input) {
1776
- const identifiers = /* @__PURE__ */ new Map();
1777
- const filePaths = [...new Set(input.declarations.map((declaration) => declaration.filePath))].sort((left, right) => compareSourceText({
1778
- left,
1779
- right
1780
- }));
1781
- for (const filePath of filePaths) identifiers.set(filePath, `serverModule${identifiers.size}`);
1782
- return {
1783
- imports: filePaths.map((filePath) => {
1784
- return {
1785
- identifier: identifiers.get(filePath) ?? "",
1786
- specifier: getModuleSpecifier({
1787
- path: input.path,
1788
- fromFilePath: input.filePath,
1789
- toFilePath: filePath
1790
- })
1791
- };
1792
- }),
1793
- getIdentifier: (filePath) => identifiers.get(filePath) ?? ""
1794
- };
1795
- }
1796
- /** Renders registration imports independently of final browser output. */
1797
- function renderGeneratedBuildModule(input) {
1798
- const modules = createModuleNamespaces({
1799
- declarations: input.declarations,
1800
- filePath: input.filePath,
1801
- path: input.path
1802
- });
1803
- return {
1804
- filePath: input.filePath,
1805
- source: [
1806
- "// Code generated by Vorma. DO NOT EDIT.",
1807
- "",
1808
- ...modules.imports.map((moduleImport) => {
1809
- const specifier = moduleImport.specifier.split("/").map(encodeURIComponent).join("/");
1810
- return `import * as ${moduleImport.identifier} from ${JSON.stringify(specifier)};`;
1811
- }),
1812
- "",
1813
- "/** Every registerable declaration that the server project exports, in source order. */",
1814
- ...input.declarations.length === 0 ? ["export const registeredDeclarations = [];"] : [
1815
- "export const registeredDeclarations = [",
1816
- ...input.declarations.map((declaration) => {
1817
- return `\t${modules.getIdentifier(declaration.filePath)}[${JSON.stringify(declaration.exportName)}],`;
1818
- }),
1819
- "];"
1820
- ],
1821
- ""
1822
- ].join("\n")
1823
- };
1824
- }
1825
- /** Renders the declaration that augments `@vorma/server` with this application's patterns, search types, and asset keys. */
1826
- function renderGeneratedServerContracts(input) {
1827
- const serverModuleSpecifier = JSON.stringify(FRAMEWORK_IDENTITY_PROTOCOL.moduleSpecifiers.server);
1828
- const routeSearchTypes = input.model.segments.map((segment) => {
1829
- const search = segment.searchOwnerInputs.length === 0 ? "undefined" : segment.searchOwnerInputs.map((text) => indentRenderedText(text, 1)).join(" &\n ");
1830
- return `\t\t\treadonly ${JSON.stringify(segment.pattern)}: ${search};`;
1831
- });
1832
- const modules = createModuleNamespaces({
1833
- declarations: input.model.serverDeclarations,
1834
- filePath: input.filePath,
1835
- path: input.path
1836
- });
1837
- return {
1838
- filePath: input.filePath,
1839
- source: [
1840
- "// Code generated by Vorma. DO NOT EDIT.",
1841
- "",
1842
- `import ${serverModuleSpecifier};`,
1843
- ...modules.imports.map((moduleImport) => {
1844
- return `import type * as ${moduleImport.identifier} from ${JSON.stringify(moduleImport.specifier)};`;
1845
- }),
1846
- ...renderEffectModuleImports(input.effectModules),
1847
- "",
1848
- ...input.typeAliases.length === 0 ? [] : [...input.typeAliases, ""],
1849
- `declare module ${serverModuleSpecifier} {`,
1850
- " interface Register {",
1851
- " readonly declarations: readonly [",
1852
- ...input.model.serverDeclarations.map((declaration) => {
1853
- return `\t\t\t(typeof ${modules.getIdentifier(declaration.filePath)})[${JSON.stringify(declaration.exportName)}],`;
1854
- }),
1855
- " ];",
1856
- ...renderPatternRegistration(input.model.segments.map((segment) => segment.pattern)),
1857
- " readonly routeSearch: {",
1858
- ...routeSearchTypes,
1859
- " };",
1860
- renderAssetKeyDeclaration({
1861
- assets: input.publicAssets.assets,
1862
- depth: 2,
1863
- prefix: " readonly assets:"
1864
- }),
1865
- " }",
1866
- "}",
1867
- ""
1868
- ].join("\n")
1869
- };
1870
- }
1871
- /** One type-only namespace import per Effect module that rendered contract types name. */
1872
- function renderEffectModuleImports(effectModules) {
1873
- return effectModules.map((moduleName) => {
1874
- return `import type * as ${moduleName} from ${JSON.stringify(`effect/${moduleName}`)};`;
1875
- });
1876
- }
1877
- function renderPatternRegistration(patterns) {
1878
- if (patterns.length === 0) return [" readonly patterns: never;"];
1879
- return [" readonly patterns:", ...patterns.map((pattern, index) => {
1880
- const terminator = index === patterns.length - 1 ? ";" : "";
1881
- return `\t\t\t| ${JSON.stringify(pattern)}${terminator}`;
1882
- })];
1883
- }
1884
- //#endregion
1885
- //#region src/build/render-client-contracts.ts
1886
- function renderGeneratedClientContracts(input) {
1887
- function toSpecifier(filePath) {
1888
- return JSON.stringify(getModuleSpecifier({
1889
- path: input.path,
1890
- fromFilePath: input.filePath,
1891
- toFilePath: filePath
1892
- }));
1893
- }
1894
- const viewModuleIdentifiers = new Map(input.model.clientViewModules.map((filePath, index) => {
1895
- return [filePath, `ClientViewModule${index}`];
1896
- }));
1897
- const uiTypes = input.model.segments.map((segment) => {
1898
- const moduleIdentifier = segment.view === void 0 ? void 0 : viewModuleIdentifiers.get(segment.view.filePath);
1899
- return renderTypeEntry(segment.pattern, {
1900
- input: segment.loader?.input ?? "undefined",
1901
- output: segment.loader?.output ?? "undefined",
1902
- search: segment.searchOwnerInputs.length === 0 ? "undefined" : segment.searchOwnerInputs.join(" &\n "),
1903
- clientLoader: segment.view === void 0 || moduleIdentifier === void 0 ? "undefined" : `GetUiViewClientLoaderContract<(typeof ${moduleIdentifier})[${JSON.stringify(segment.view.exportName)}]>`
1904
- });
1905
- });
1906
- const endpointTypes = input.model.endpoints.map((endpoint) => {
1907
- return renderTypeEntry(`${endpoint.method} ${endpoint.pattern}`, {
1908
- kind: JSON.stringify(endpoint.operation),
1909
- input: endpoint.inputTransport === "formData" ? "FormData" : endpoint.input ?? "undefined",
1910
- output: endpoint.output,
1911
- errors: renderErrorTypes(endpoint.errors)
1912
- });
1913
- });
1914
- const application = input.model.clientApplication;
1915
- const entryImport = application.exportName === "default" ? `import type ClientEntry from ${toSpecifier(application.filePath)};` : `import type { ${application.exportName} as ClientEntry } from ${toSpecifier(application.filePath)};`;
1916
- return {
1917
- filePath: input.filePath,
1918
- source: [
1919
- "// Code generated by Vorma. DO NOT EDIT.",
1920
- "",
1921
- "import type {",
1922
- " GetClientApplicationServiceContracts,",
1923
- ...input.model.clientViewModules.length === 0 ? [] : [" GetUiViewClientLoaderContract,"],
1924
- `} from ${JSON.stringify(FRAMEWORK_IDENTITY_PROTOCOL.moduleSpecifiers.clientReact)};`,
1925
- entryImport,
1926
- ...input.model.clientViewModules.map((filePath) => {
1927
- return `import type * as ${viewModuleIdentifiers.get(filePath)} from ${toSpecifier(filePath)};`;
1928
- }),
1929
- ...renderEffectModuleImports(input.inspection.effectModules),
1930
- "",
1931
- "type ApplicationServiceContracts =",
1932
- " GetClientApplicationServiceContracts<typeof ClientEntry>;",
1933
- "",
1934
- ...input.inspection.typeAliases.length === 0 ? [] : [...input.inspection.typeAliases, ""],
1935
- "export type UiContracts = {",
1936
- ...uiTypes,
1937
- "};",
1938
- "",
1939
- "export type EndpointContracts = {",
1940
- ...endpointTypes,
1941
- "};",
1942
- "",
1943
- ...renderAugmentation(FRAMEWORK_IDENTITY_PROTOCOL.moduleSpecifiers.client),
1944
- ""
1945
- ].join("\n")
1946
- };
1947
- }
1948
- function renderAugmentation(moduleSpecifier) {
1949
- return [
1950
- `declare module ${JSON.stringify(moduleSpecifier)} {`,
1951
- " interface ClientApplicationContracts {",
1952
- " readonly ui: UiContracts;",
1953
- " readonly endpoints: EndpointContracts;",
1954
- " readonly clientOnlyServices: ApplicationServiceContracts[\"clientOnlyServices\"];",
1955
- " readonly universalServices: ApplicationServiceContracts[\"universalServices\"];",
1956
- " }",
1957
- "}"
1958
- ];
1959
- }
1960
- /** One keyed entry of a contract map, its properties one per line. */
1961
- function renderTypeEntry(key, properties) {
1962
- return [
1963
- `\treadonly ${JSON.stringify(key)}: {`,
1964
- ...Object.entries(properties).map(([name, type]) => {
1965
- return `\t\treadonly ${name}: ${type};`;
1966
- }),
1967
- " };"
1968
- ].join("\n");
1969
- }
1970
- /** An operation's declared errors, keyed by their literal tags. */
1971
- function renderErrorTypes(errors) {
1972
- if (errors.length === 0) return "{}";
1973
- return [
1974
- "{",
1975
- ...errors.map((error) => {
1976
- return `\t\t\treadonly ${JSON.stringify(error.tag)}: ${indentRenderedText(error.fields, 1)};`;
1977
- }),
1978
- " }"
1979
- ].join("\n");
1980
- }
1981
- //#endregion
1982
- //#region src/build/render-client-transport.ts
1983
- const ADAPTER_EXPORT_BY_TYPE = {
1984
- option: "optionTransportType",
1985
- redacted: "redactedTransportType",
1986
- chunk: "chunkTransportType",
1987
- duration: "durationTransportType",
1988
- "big-decimal": "bigDecimalTransportType",
1989
- "date-time-utc": "dateTimeUtcTransportType",
1990
- "date-time-zoned": "dateTimeZonedTransportType",
1991
- "hash-map": "hashMapTransportType",
1992
- "hash-set": "hashSetTransportType"
1993
- };
1994
- /**
1995
- * Selects generic value adapters without generating application schemas or route
1996
- * metadata. The shared adapters use pure initializers so that unused Effect
1997
- * export reads do not retain optional data modules in the browser bundle.
1998
- */
1999
- function renderGeneratedClientTransport(input) {
2000
- const entries = EFFECT_TRANSPORT_TYPES.flatMap(({ name }) => {
2001
- return input.typeNames.has(name) ? [ADAPTER_EXPORT_BY_TYPE[name]] : [];
2002
- });
2003
- const moduleSpecifier = JSON.stringify(FRAMEWORK_IDENTITY_PROTOCOL.moduleSpecifiers.client);
2004
- return {
2005
- filePath: input.filePath,
2006
- source: [
2007
- "// Code generated by Vorma. DO NOT EDIT.",
2008
- "",
2009
- ...entries.length === 0 ? [] : [`import { ${entries.join(", ")} } from ${moduleSpecifier};`],
2010
- `import type { TransportTypeEntry } from ${moduleSpecifier};`,
2011
- "",
2012
- "export function getTransportTypeEntries(): ReadonlyArray<TransportTypeEntry> {",
2013
- ` return [${entries.join(", ")}];`,
2014
- "}",
2015
- ""
2016
- ].join("\n")
2017
- };
2018
- }
2019
- //#endregion
2020
- //#region src/build/generate-application.ts
2021
- /** Writes correlated generated sources and verifies their native TypeScript inclusion. */
2022
- function writeAndCheckApplicationGeneration(generation, knownRootFiles) {
2023
- return Effect.gen(function* () {
2024
- yield* Effect.all([writeApplicationGeneration(generation), writeGeneratedServerApplication({ generation })], {
2025
- concurrency: 2,
2026
- discard: true
2027
- });
2028
- yield* checkGeneratedTypeInclusion({
2029
- ...generation.config,
2030
- ...knownRootFiles === void 0 ? {} : { knownRootFiles },
2031
- serverFiles: [generation.config.generatedServerContractsFilePath],
2032
- clientFiles: [generation.clientContracts.filePath, generation.publicAssets.types.filePath]
2033
- });
2034
- });
2035
- }
2036
- /** Reads both projects through the checker, captures public assets, and prepares generated build inputs without publishing them. */
2037
- function prepareApplicationGeneration(sourceLayout) {
2038
- return Effect.gen(function* () {
2039
- return yield* assembleApplicationGeneration({
2040
- sourceLayout,
2041
- ...yield* Effect.all({
2042
- inspection: inspectApplicationSources(sourceLayout),
2043
- publicAssets: collectPublicAssets(sourceLayout)
2044
- }, { concurrency: "unbounded" })
2045
- });
2046
- });
2047
- }
2048
- /** Reads declarations and public URL call sites independently of public asset hashing. */
2049
- function inspectApplicationSources(sourceLayout) {
2050
- return withTypescriptProjects({
2051
- configDirectory: sourceLayout.configDirectory,
2052
- serverTsconfigFilePath: sourceLayout.serverTsconfigFilePath,
2053
- clientTsconfigFilePath: sourceLayout.clientTsconfigFilePath,
2054
- useFn: (projects) => {
2055
- return inspectProjects({
2056
- projects,
2057
- layout: sourceLayout
2058
- });
2059
- }
2060
- });
2061
- }
2062
- /** Prepares correlated build inputs from an inspection and a captured public directory. */
2063
- function assembleApplicationGeneration(input) {
2064
- return Effect.gen(function* () {
2065
- const path = yield* Path$1.Path;
2066
- const { sourceLayout, inspection, publicAssets } = input;
2067
- const model = compileApplicationModel(inspection);
2068
- if (Result.isFailure(model)) return yield* Effect.fail({
2069
- _tag: "ApplicationModelIssues",
2070
- issues: model.failure
2071
- });
2072
- const { publicFilemap } = publicAssets;
2073
- for (const site of inspection.publicUrlSites) if (!Object.hasOwn(publicFilemap, normalizePublicAssetKey(site.key))) return yield* Effect.fail({
2074
- _tag: "UnknownPublicAssetKey",
2075
- filePath: site.filePath,
2076
- position: site.start,
2077
- key: site.key
2078
- });
2079
- return {
2080
- config: sourceLayout,
2081
- model: model.success,
2082
- inspection,
2083
- publicAssets,
2084
- clientContracts: renderGeneratedClientContracts({
2085
- model: model.success,
2086
- inspection,
2087
- filePath: path.join(sourceLayout.clientGeneratedDirectory, ARTIFACT_PROTOCOL.generatedClientContractsFileName),
2088
- path
2089
- }),
2090
- clientTransport: renderGeneratedClientTransport({
2091
- typeNames: inspection.transportTypeNames,
2092
- filePath: path.join(sourceLayout.clientGeneratedDirectory, ARTIFACT_PROTOCOL.generatedClientTransportFileName)
2093
- })
2094
- };
2095
- });
2096
- }
2097
- /**
2098
- * Writes the client's generated declarations, transport adapters, and an ignore
2099
- * file in each owned output directory. Cleanup uses the complete client/server
2100
- * output plan, so server source can be written concurrently with this work.
2101
- */
2102
- function writeApplicationGeneration(generation) {
2103
- const outputs = [
2104
- generation.publicAssets.types,
2105
- generation.clientContracts,
2106
- generation.clientTransport
2107
- ];
2108
- const plannedSourceFiles = [
2109
- ...outputs.map((output) => output.filePath),
2110
- generation.config.generatedRegistrationFilePath,
2111
- generation.config.generatedServerContractsFilePath
2112
- ];
2113
- return Effect.forEach([...generation.config.ignoreFilePaths.map((filePath) => {
2114
- return {
2115
- filePath,
2116
- source: ARTIFACT_PROTOCOL.ignoreFileSource
2117
- };
2118
- }), ...outputs], writeGeneratedSource, {
2119
- concurrency: 16,
2120
- discard: true
2121
- }).pipe(Effect.andThen(Effect.forEach([.../* @__PURE__ */ new Set([generation.config.clientGeneratedDirectory, generation.config.serverGeneratedDirectory])], (directory) => {
2122
- return removeStaleGeneratedSource({
2123
- directory,
2124
- plannedSourceFiles
2125
- });
2126
- }, {
2127
- concurrency: 2,
2128
- discard: true
2129
- })));
2130
- }
2131
- /** Matches the generated source files the build owns by name, so a name an earlier build used and this one does not is removed. */
2132
- const GENERATED_SOURCE_FILE_NAME = /(?:\.gen\.(?:d\.)?ts|\.mjs)$/;
2133
- function removeStaleGeneratedSource(input) {
2134
- return Effect.gen(function* () {
2135
- const fileSystem = yield* FileSystem$1.FileSystem;
2136
- const path = yield* Path$1.Path;
2137
- const plannedSourceFiles = new Set(input.plannedSourceFiles);
2138
- const stale = (yield* fileSystem.readDirectory(input.directory)).map((entry) => path.join(input.directory, entry)).filter((filePath) => {
2139
- return GENERATED_SOURCE_FILE_NAME.test(filePath) && !plannedSourceFiles.has(filePath);
2140
- }).sort((left, right) => compareSourceText({
2141
- left,
2142
- right
2143
- }));
2144
- yield* Effect.forEach(stale, (filePath) => fileSystem.remove(filePath), {
2145
- concurrency: 16,
2146
- discard: true
2147
- });
2148
- }).pipe(Effect.mapError((cause) => {
2149
- return {
2150
- _tag: "GeneratedApplicationWriteError",
2151
- filePath: input.directory,
2152
- cause
2153
- };
2154
- }));
2155
- }
2156
- /**
2157
- * Writes the internal registration module and the application
2158
- * contract types before application startup.
2159
- */
2160
- function writeGeneratedServerApplication(input) {
2161
- return Effect.gen(function* () {
2162
- const path = yield* Path$1.Path;
2163
- const { config, model, inspection } = input.generation;
2164
- const buildModule = renderGeneratedBuildModule({
2165
- declarations: model.serverDeclarations,
2166
- filePath: config.generatedRegistrationFilePath,
2167
- path
2168
- });
2169
- const contracts = renderGeneratedServerContracts({
2170
- model,
2171
- publicAssets: input.generation.publicAssets,
2172
- typeAliases: inspection.typeAliases,
2173
- effectModules: inspection.effectModules,
2174
- filePath: config.generatedServerContractsFilePath,
2175
- path
2176
- });
2177
- yield* Effect.forEach([buildModule, contracts], writeGeneratedSource, {
2178
- concurrency: 2,
2179
- discard: true
2180
- });
2181
- });
2182
- }
2183
- function writeGeneratedSource(output) {
2184
- return writeOutputFile(output).pipe(Effect.mapError((cause) => {
2185
- return {
2186
- _tag: "GeneratedApplicationWriteError",
2187
- filePath: output.filePath,
2188
- cause
2189
- };
2190
- }));
2191
- }
2192
- //#endregion
2193
- //#region src/build/output-ownership.ts
2194
- const Owner = Schema.fromJsonString(Schema.Struct({
2195
- pid: Schema.Int.check(Schema.isGreaterThan(0)),
2196
- kind: Schema.Literals(["build", "dev"]),
2197
- directories: Schema.NonEmptyArray(Schema.String)
2198
- }));
2199
- function getRemovalCommand(lockPath) {
2200
- if (platform === "win32") return `PowerShell: Remove-Item -LiteralPath '${lockPath.replaceAll("'", "''")}'`;
2201
- return `rm -- '${lockPath.replaceAll("'", "'\"'\"'")}'`;
2202
- }
2203
- function removeOwnedLock(input) {
2204
- return input.fileSystem.remove(input.lockPath, { force: true }).pipe(Effect.catch((cause) => {
2205
- return Effect.logError(`Could not remove Vorma lock ${input.lockPath}. If the owner has stopped, run ${getRemovalCommand(input.lockPath)}`, cause);
2206
- }));
2207
- }
2208
- /** Resolves aliases even when the output directory has not been created. */
2209
- function canonicalizeOutputPath(filePath) {
2210
- return Effect.gen(function* () {
2211
- const fileSystem = yield* FileSystem.FileSystem;
2212
- const path = yield* Path$1.Path;
2213
- let current = path.resolve(filePath);
2214
- const suffix = [];
2215
- while (true) {
2216
- const resolved = yield* Effect.result(fileSystem.realPath(current));
2217
- if (Result.isSuccess(resolved)) return path.join(resolved.success, ...suffix);
2218
- if (resolved.failure.reason._tag !== "NotFound" || path.dirname(current) === current) return yield* Effect.fail({
2219
- _tag: "OutputOwnershipIssue",
2220
- directory: filePath,
2221
- message: `Cannot resolve output path ${filePath}`,
2222
- cause: resolved.failure
2223
- });
2224
- const parent = path.dirname(current);
2225
- suffix.unshift(path.relative(parent, current));
2226
- current = parent;
2227
- }
2228
- });
2229
- }
2230
- /** Checks canonical containment before removing or replacing generated output. */
2231
- function checkOutputPath(input) {
2232
- return Effect.gen(function* () {
2233
- const path = yield* Path$1.Path;
2234
- const relativePath = path.relative(input.rootDirectory, input.filePath);
2235
- const [root, target] = yield* Effect.all([canonicalizeOutputPath(input.rootDirectory), canonicalizeOutputPath(input.filePath)], { concurrency: 2 });
2236
- if (!containsRelativePath({
2237
- relativePath,
2238
- separator: path.sep,
2239
- absolute: path.isAbsolute(relativePath)
2240
- }) || target !== path.join(root, relativePath)) return yield* Effect.fail({
2241
- _tag: "OutputOwnershipIssue",
2242
- directory: input.filePath,
2243
- message: `Generated output path leaves its directory: ${input.filePath}`
2244
- });
2245
- });
2246
- }
2247
- /** Owns the application's generated output until the build or development scope closes. */
2248
- function acquireOutputOwnership(input) {
2249
- return Effect.gen(function* () {
2250
- const fileSystem = yield* FileSystem.FileSystem;
2251
- const path = yield* Path$1.Path;
2252
- function toOwnedRoot(directory) {
2253
- return fileSystem.makeDirectory(directory, { recursive: true }).pipe(Effect.andThen(canonicalizeOutputPath(directory)));
2254
- }
2255
- const lockRoot = yield* toOwnedRoot(input.lockDirectory);
2256
- const boundaryRoots = yield* Effect.forEach(input.boundaryDirectories, toOwnedRoot, { concurrency: "unbounded" });
2257
- const lockPath = path.join(lockRoot, "build.lock");
2258
- const owner = {
2259
- pid,
2260
- kind: input.kind,
2261
- directories: [lockRoot, ...boundaryRoots]
2262
- };
2263
- yield* Effect.acquireRelease(Effect.scoped(fileSystem.open(lockPath, {
2264
- flag: "wx",
2265
- mode: 384
2266
- })).pipe(Effect.catch((cause) => {
2267
- return Effect.gen(function* () {
2268
- if (cause.reason._tag !== "AlreadyExists") return yield* Effect.fail(cause);
2269
- const recorded = Option.getOrUndefined(yield* fileSystem.readFileString(lockPath).pipe(Effect.flatMap(Schema.decodeUnknownEffect(Owner)), Effect.option));
2270
- return yield* Effect.fail({
2271
- _tag: "OutputOwnershipIssue",
2272
- directory: lockRoot,
2273
- message: [
2274
- `Cannot start ${input.kind}: generated output is locked.`,
2275
- `Lockfile: ${lockPath}`,
2276
- `Recorded owner PID: ${recorded?.pid ?? "unavailable"}`,
2277
- `Recorded operation: ${recorded?.kind ?? "unavailable"}`,
2278
- `Requested outputs: ${owner.directories.join(", ")}`,
2279
- "Wait for the owning process to finish. If it has stopped, remove the leftover lock and retry:",
2280
- ` ${getRemovalCommand(lockPath)}`
2281
- ].join("\n")
2282
- });
2283
- });
2284
- })), () => removeOwnedLock({
2285
- fileSystem,
2286
- lockPath
2287
- }));
2288
- yield* fileSystem.writeFileString(lockPath, JSON.stringify(owner));
2289
- }).pipe(Effect.mapError((cause) => {
2290
- return cause._tag === "OutputOwnershipIssue" ? cause : {
2291
- _tag: "OutputOwnershipIssue",
2292
- directory: input.lockDirectory,
2293
- message: "Cannot acquire Vorma output ownership",
2294
- cause
2295
- };
2296
- }));
2297
- }
2298
- //#endregion
2299
- //#region src/build/publish-artifacts.ts
2300
- /** Prepares one public output before Vite adds its browser files to it. */
2301
- function preparePublicOutput(config) {
2302
- return Effect.gen(function* () {
2303
- const fileSystem = yield* FileSystem$1.FileSystem;
2304
- const directory = config.publicDirectory;
2305
- yield* checkOutputPath({
2306
- filePath: directory,
2307
- rootDirectory: config.clientGeneratedDirectory
2308
- });
2309
- yield* fileSystem.remove(directory, {
2310
- force: true,
2311
- recursive: true
2312
- });
2313
- yield* fileSystem.makeDirectory(directory, { recursive: true });
2314
- });
2315
- }
2316
- /** Publishes private metadata for the completed browser build. */
2317
- function publishApplicationArtifacts(input) {
2318
- return Effect.gen(function* () {
2319
- const fileSystem = yield* FileSystem$1.FileSystem;
2320
- const path = yield* Path$1.Path;
2321
- const config = input.generation.config;
2322
- const metadataPath = path.join(config.serverGeneratedDirectory, ARTIFACT_PROTOCOL.productionMetadataFileName);
2323
- const temporaryPath = `${metadataPath}.tmp`;
2324
- yield* fileSystem.writeFileString(temporaryPath, JSON.stringify(input.artifacts.metadata));
2325
- yield* fileSystem.rename(temporaryPath, metadataPath);
2326
- return {
2327
- clientBuildId: input.artifacts.metadata.clientBuildId,
2328
- publicDirectory: config.publicDirectory
2329
- };
2330
- }).pipe(Effect.mapError((cause) => {
2331
- return {
2332
- _tag: "ArtifactPublicationIssue",
2333
- cause
2334
- };
2335
- }));
2336
- }
2337
- //#endregion
2338
- //#region src/build/source-layout.ts
2339
- /** Loads the configured source layout before any application module executes. */
2340
- function loadApplicationSourceLayout(config) {
2341
- return Effect.gen(function* () {
2342
- const fileSystem = yield* FileSystem$1.FileSystem;
2343
- const path = yield* Path$1.Path;
2344
- const requiredPaths = {
2345
- serverRootDirectory: {
2346
- field: "serverTsconfigFile",
2347
- path: path.dirname(config.serverTsconfigFilePath),
2348
- expectedType: "Directory"
2349
- },
2350
- clientRootDirectory: {
2351
- field: "clientTsconfigFile",
2352
- path: path.dirname(config.clientTsconfigFilePath),
2353
- expectedType: "Directory"
2354
- },
2355
- serverTsconfigFilePath: {
2356
- field: "serverTsconfigFile",
2357
- path: config.serverTsconfigFilePath,
2358
- expectedType: "File"
2359
- },
2360
- clientEntryFilePath: {
2361
- field: "clientEntry",
2362
- path: config.clientEntryFilePath,
2363
- expectedType: "File"
2364
- },
2365
- clientTsconfigFilePath: {
2366
- field: "clientTsconfigFile",
2367
- path: config.clientTsconfigFilePath,
2368
- expectedType: "File"
2369
- },
2370
- publicAssetsSourceDirectory: {
2371
- field: "publicAssetsDir",
2372
- path: config.publicAssetsSourceDirectory,
2373
- expectedType: "Directory"
2374
- },
2375
- viteConfigFilePath: {
2376
- field: "viteConfigFile",
2377
- path: config.viteConfigFilePath,
2378
- expectedType: "File"
2379
- }
2380
- };
2381
- const criticalCssRequirement = config.criticalCssEntryFilePath === void 0 ? void 0 : {
2382
- field: "criticalCssEntry",
2383
- path: config.criticalCssEntryFilePath,
2384
- expectedType: "File"
2385
- };
2386
- function inspectSourcePath(requirement) {
2387
- return Effect.all({
2388
- stat: Effect.result(fileSystem.stat(requirement.path)),
2389
- canonicalPath: Effect.result(fileSystem.realPath(requirement.path))
2390
- }).pipe(Effect.map((inspection) => {
2391
- if (Result.isFailure(inspection.stat)) return Result.fail({
2392
- ...requirement,
2393
- cause: inspection.stat.failure
2394
- });
2395
- if (inspection.stat.success.type !== requirement.expectedType) return Result.fail({
2396
- ...requirement,
2397
- actualType: inspection.stat.success.type
2398
- });
2399
- if (Result.isFailure(inspection.canonicalPath)) return Result.fail({
2400
- ...requirement,
2401
- cause: inspection.canonicalPath.failure
2402
- });
2403
- return Result.succeed(inspection.canonicalPath.success);
2404
- }));
2405
- }
2406
- const inspections = yield* Effect.all({
2407
- ...Record.map(requiredPaths, inspectSourcePath),
2408
- criticalCssEntryFilePath: criticalCssRequirement === void 0 ? Effect.succeed(Result.succeed(void 0)) : inspectSourcePath(criticalCssRequirement)
2409
- }, { concurrency: "unbounded" });
2410
- const issues = Object.values(inspections).flatMap((inspection) => {
2411
- return Result.isFailure(inspection) ? [inspection.failure] : [];
2412
- });
2413
- const canonicalPaths = Result.all(inspections);
2414
- if (Result.isFailure(canonicalPaths)) return yield* Effect.fail({
2415
- _tag: "ApplicationSourceLayoutIssues",
2416
- issues
2417
- });
2418
- const { serverRootDirectory, clientRootDirectory, criticalCssEntryFilePath, ...canonical } = canonicalPaths.success;
2419
- if (serverRootDirectory === clientRootDirectory) return yield* Effect.fail({
2420
- _tag: "ApplicationSourceLayoutIssues",
2421
- issues: [{
2422
- field: "clientTsconfigFile",
2423
- path: config.clientTsconfigFilePath,
2424
- expectedType: "File",
2425
- message: `serverTsconfigFile and clientTsconfigFile must be in different directories. Both project roots resolve to ${serverRootDirectory} and would share the same _vorma output.`
2426
- }]
2427
- });
2428
- const canonicalConfig = {
2429
- ...config,
2430
- ...canonical,
2431
- serverProjectDirectory: path.dirname(canonical.serverTsconfigFilePath),
2432
- ...criticalCssEntryFilePath === void 0 ? {} : { criticalCssEntryFilePath }
2433
- };
2434
- const publicSource = canonicalConfig.publicAssetsSourceDirectory;
2435
- const publicOutput = canonicalConfig.publicDirectory;
2436
- if ([path.relative(publicSource, publicOutput), path.relative(publicOutput, publicSource)].some((relativePath) => {
2437
- return containsRelativePath({
2438
- relativePath,
2439
- separator: path.sep,
2440
- absolute: path.isAbsolute(relativePath)
2441
- });
2442
- })) return yield* Effect.fail({
2443
- _tag: "ApplicationSourceLayoutIssues",
2444
- issues: [{
2445
- field: "publicAssetsDir",
2446
- path: publicSource,
2447
- expectedType: "Directory",
2448
- message: `Public asset source ${publicSource} overlaps generated output ${publicOutput}; the build cannot replace its own input or capture its own output.`
2449
- }]
2450
- });
2451
- return canonicalConfig;
2452
- });
2453
- }
2454
- //#endregion
2455
- //#region src/build/inspect-css-artifacts.ts
2456
- /** Records dependencies from compiled stylesheets for nonblocking preload hints. */
2457
- function inspectCssArtifacts(input) {
2458
- const references = /* @__PURE__ */ new Map();
2459
- for (const stylesheet of input.stylesheets) {
2460
- const urls = [];
2461
- references.set(stylesheet.url, urls);
2462
- const name = stylesheet.url === null ? "critical CSS" : stylesheet.url;
2463
- const parsed = Result.try({
2464
- try: () => {
2465
- return transform({
2466
- filename: name,
2467
- code: Buffer.from(stylesheet.source),
2468
- analyzeDependencies: true
2469
- });
2470
- },
2471
- catch: (cause) => `Cannot inspect ${name}: ${String(cause)}`
2472
- });
2473
- if (Result.isFailure(parsed)) return Result.fail(parsed.failure);
2474
- const dependencies = parsed.success.dependencies;
2475
- if (dependencies === void 0) continue;
2476
- for (const dependency of dependencies) {
2477
- if (dependency.type !== "url" && dependency.type !== "import") continue;
2478
- const reference = dependency.url;
2479
- if (reference.startsWith("#") || reference.startsWith("data:")) continue;
2480
- const resolved = Result.try({
2481
- try: () => {
2482
- return new URL(reference, `https://vorma.invalid${stylesheet.url === null ? "/" : stylesheet.url}`);
2483
- },
2484
- catch: () => `Invalid CSS URL: ${reference}`
2485
- });
2486
- if (Result.isFailure(resolved)) return Result.fail(resolved.failure);
2487
- const absolute = resolved.success;
2488
- urls.push({
2489
- url: absolute.origin === "https://vorma.invalid" ? stylesheet.url === null && !reference.startsWith("/") ? reference : absolute.pathname + absolute.search + absolute.hash : reference.startsWith("//") ? reference : absolute.href,
2490
- kind: dependency.type
2491
- });
2492
- }
2493
- }
2494
- return Result.succeed(references);
2495
- }
2496
- //#endregion
2497
- //#region src/build/assemble-artifacts.ts
2498
- /** Joins published file metadata and checks paths, collisions, and module projections. */
2499
- function assembleApplicationArtifacts(input) {
2500
- return Effect.gen(function* () {
2501
- const { generation, client } = input;
2502
- const base = generation.config.publicAssetsBasePath;
2503
- function toPublicUrl(fileName) {
2504
- return base + fileName.split("/").map(encodeURIComponent).join("/");
2505
- }
2506
- const byFile = /* @__PURE__ */ new Map();
2507
- const byUrl = /* @__PURE__ */ new Map();
2508
- const directories = /* @__PURE__ */ new Set();
2509
- /** Records one output, or the artifact already recorded for the same bytes at the same file. */
2510
- function addFile(output) {
2511
- const parsed = parseArtifactFileName(output.fileName);
2512
- if (Result.isFailure(parsed)) return Result.fail(parsed.failure);
2513
- if (output.immutable && !output.fileName.startsWith(ARTIFACT_PROTOCOL.publicOutputFileNamePrefix)) return Result.fail(`Hashed public asset must start with ${ARTIFACT_PROTOCOL.publicOutputFileNamePrefix}: ${output.fileName}`);
2514
- const digest = output.digest;
2515
- const existing = byFile.get(output.fileName);
2516
- if (existing !== void 0) {
2517
- if (existing.digest !== digest || existing.url !== output.url || existing.immutable !== output.immutable) return Result.fail(`Conflicting artifact outputs: ${output.fileName}`);
2518
- return Result.succeed(existing);
2519
- }
2520
- if (byUrl.has(output.url)) return Result.fail(`Conflicting artifact URL: ${output.url}`);
2521
- const parts = output.fileName.split("/");
2522
- const parents = parts.slice(0, -1).map((_part, index) => parts.slice(0, index + 1).join("/"));
2523
- if (directories.has(output.fileName) || parents.some((parent) => byFile.has(parent))) return Result.fail(`Artifact file and directory conflict: ${output.fileName}`);
2524
- const artifact = output;
2525
- byFile.set(output.fileName, artifact);
2526
- byUrl.set(output.url, artifact);
2527
- for (const parent of parents) directories.add(parent);
2528
- return Result.succeed(artifact);
2529
- }
2530
- for (const asset of generation.publicAssets.assets) yield* Effect.fromResult(addFile({
2531
- fileName: asset.fileName,
2532
- url: asset.url,
2533
- digest: asset.digest,
2534
- immutable: true
2535
- }));
2536
- const bundleArtifacts = [];
2537
- for (const output of client.outputs) {
2538
- const source = typeof output.source === "string" ? new TextEncoder().encode(output.source) : output.source;
2539
- const artifact = yield* Effect.fromResult(addFile({
2540
- fileName: output.fileName,
2541
- digest: toContentDigest(source),
2542
- url: toPublicUrl(output.fileName),
2543
- immutable: byFile.get(output.fileName)?.immutable ?? output.immutable
2544
- }));
2545
- bundleArtifacts.push({
2546
- ...artifact,
2547
- source
2548
- });
2549
- }
2550
- const cssReferences = yield* Effect.fromResult(inspectCssArtifacts({ stylesheets: [{
2551
- url: null,
2552
- source: new TextEncoder().encode(client.criticalCss)
2553
- }, ...bundleArtifacts.filter((artifact) => artifact.fileName.endsWith(".css")).map((artifact) => {
2554
- return {
2555
- url: artifact.url,
2556
- source: artifact.source
2557
- };
2558
- })] }));
2559
- const publicFilemap = generation.publicAssets.publicFilemap;
2560
- function collectCssAssets(stylesheets) {
2561
- const visited = /* @__PURE__ */ new Set();
2562
- const assets = /* @__PURE__ */ new Set();
2563
- const pending = [...stylesheets];
2564
- while (pending.length > 0) {
2565
- const stylesheet = pending.pop();
2566
- if (stylesheet === void 0 || visited.has(stylesheet)) continue;
2567
- visited.add(stylesheet);
2568
- for (const reference of cssReferences.get(stylesheet) ?? []) if (reference.kind === "import") pending.push(reference.url);
2569
- else assets.add(reference.url);
2570
- }
2571
- return [...assets];
2572
- }
2573
- function addCssAssetUrls(module) {
2574
- return {
2575
- ...module,
2576
- staticAssetUrls: [.../* @__PURE__ */ new Set([...module.staticAssetUrls, ...collectCssAssets(module.cssBundleUrls)])]
2577
- };
2578
- }
2579
- const entryModule = addCssAssetUrls(client.clientEntry);
2580
- const clientEntry = {
2581
- ...entryModule,
2582
- staticAssetUrls: [.../* @__PURE__ */ new Set([...entryModule.staticAssetUrls, ...collectCssAssets([null])])]
2583
- };
2584
- const clientViews = Object.fromEntries(Object.entries(client.clientViews).map(([pattern, module]) => {
2585
- return [pattern, {
2586
- ...addCssAssetUrls(module),
2587
- exportName: module.exportName
2588
- }];
2589
- }));
2590
- const files = [...byFile.values()].sort((left, right) => compareSourceText({
2591
- left: left.url,
2592
- right: right.url
2593
- }));
2594
- const metadata = {
2595
- runtimeMode: "production",
2596
- publicFiles: files.map((file) => {
2597
- return {
2598
- url: file.url,
2599
- fileName: file.fileName,
2600
- immutable: file.immutable
2601
- };
2602
- }),
2603
- publicFilemap,
2604
- criticalCss: client.criticalCss,
2605
- clientEntry,
2606
- clientViews
2607
- };
2608
- const { publicFiles: _publicFiles, ...browserMetadata } = metadata;
2609
- return { metadata: {
2610
- ...metadata,
2611
- clientBuildId: toContentDigest(encodeCanonicalJson({
2612
- version: 3,
2613
- metadata: browserMetadata,
2614
- contracts: projectBrowserContracts(generation.inspection),
2615
- files: files.filter((file) => file.immutable).map((file) => {
2616
- return {
2617
- url: file.url,
2618
- digest: file.digest
2619
- };
2620
- })
2621
- })).slice(0, 24)
2622
- } };
2623
- }).pipe(Effect.mapError((reason) => {
2624
- return {
2625
- _tag: "ArtifactAssemblyIssue",
2626
- reason
2627
- };
2628
- }));
2629
- }
2630
- function parseArtifactFileName(fileName) {
2631
- if (fileName.split("/").some((part) => {
2632
- return part === "" || part === "." || part === ".." || /[\\:\0]/.test(part);
2633
- })) return Result.fail(`Invalid artifact output path: ${fileName}`);
2634
- return Result.succeed(fileName);
2635
- }
2636
- //#endregion
2637
- //#region src/build/client-output-hashing.ts
2638
- /** Native naming retains Vite's content hashing and dependency URL rewriting. */
2639
- function createClientOutputHashingPlugin(immutableFiles) {
2640
- return {
2641
- name: "vorma:client-output-hashing",
2642
- enforce: "post",
2643
- outputOptions: {
2644
- order: "post",
2645
- handler(options) {
2646
- return {
2647
- ...options,
2648
- entryFileNames: ARTIFACT_PROTOCOL.publicModuleFileNamePattern,
2649
- chunkFileNames: ARTIFACT_PROTOCOL.publicModuleFileNamePattern,
2650
- assetFileNames: ARTIFACT_PROTOCOL.publicAssetFileNamePattern
2651
- };
2652
- }
2653
- },
2654
- generateBundle: {
2655
- order: "post",
2656
- handler(_options, bundle) {
2657
- for (const output of Object.values(bundle)) {
2658
- if (output.type !== "chunk") continue;
2659
- if (output.preliminaryFileName !== output.fileName) immutableFiles.add(output.fileName);
2660
- for (const file of [...output.viteMetadata?.importedCss ?? [], ...output.viteMetadata?.importedAssets ?? []]) immutableFiles.add(file);
2661
- }
2662
- }
2663
- }
2664
- };
2665
- }
2666
- //#endregion
2667
- //#region src/build/critical-css.ts
2668
- /** Finds the configured critical stylesheet through Vite's module-to-CSS association. */
2669
- function findCriticalCssFileName(input) {
2670
- if (input.sourcePath === void 0) return Result.succeed(void 0);
2671
- const sourcePath = input.sourcePath;
2672
- const stylesheets = new Set(input.outputs.flatMap((output) => {
2673
- return output.type === "chunk" && Object.hasOwn(output.modules, sourcePath) ? [...output.viteMetadata?.importedCss ?? []] : [];
2674
- }));
2675
- const stylesheet = [...stylesheets][0];
2676
- if (stylesheets.size !== 1 || stylesheet === void 0) return Result.fail("The configured critical CSS entry must produce one stylesheet in the client build");
2677
- return Result.succeed(stylesheet);
2678
- }
2679
- //#endregion
2680
- //#region src/build/bundle-client.ts
2681
- /** Builds browser entries directly into Vorma's prepared public directory. */
2682
- function bundleClient(generation) {
2683
- return Effect.gen(function* () {
2684
- const viewEntries = getClientViewEntries(generation);
2685
- const publicCss = createCssPublicAssets(generation.publicAssets);
2686
- const { result, criticalCss, cssAssets, referencedAssets, javascriptAssets, manifestFiles, immutableFiles } = yield* Effect.tryPromise(() => runClientBuild(generation, publicCss, viewEntries)).pipe(Effect.uninterruptible);
2687
- if (result === void 0) return yield* Effect.fail("The Vite application build did not produce Vorma's client environment");
2688
- const fileSystem = yield* FileSystem$1.FileSystem;
2689
- const path = yield* Path$1.Path;
2690
- yield* Effect.forEach(manifestFiles, (fileName) => {
2691
- return checkOutputPath({
2692
- filePath: path.join(generation.config.publicDirectory, fileName),
2693
- rootDirectory: generation.config.publicDirectory
2694
- }).pipe(Effect.andThen(fileSystem.remove(path.join(generation.config.publicDirectory, fileName), { force: true })));
2695
- }, {
2696
- concurrency: 2,
2697
- discard: true
2698
- });
2699
- const outputs = result.output.filter((output) => !manifestFiles.has(output.fileName));
2700
- for (const fileName of cssAssets) immutableFiles.add(fileName);
2701
- const chunkFiles = new Set(outputs.flatMap((output) => {
2702
- return output.type === "chunk" ? [output.fileName] : [];
2703
- }));
2704
- const modules = /* @__PURE__ */ new Map();
2705
- for (const output of outputs) {
2706
- if (output.type !== "chunk") continue;
2707
- const assetUrls = [];
2708
- for (const hostId of /* @__PURE__ */ new Set([output.preliminaryFileName, output.fileName])) for (const reference of javascriptAssets.get(hostId) ?? []) {
2709
- if (!output.viteMetadata?.importedAssets.has(reference.fileName.replace(/[?#].*$/s, ""))) continue;
2710
- const url = toJavascriptAssetUrl({
2711
- reference,
2712
- base: generation.config.publicAssetsBasePath
2713
- });
2714
- if (url !== void 0) assetUrls.push(url);
2715
- }
2716
- modules.set(output.fileName, {
2717
- file: output.fileName,
2718
- imports: output.imports.filter((file) => chunkFiles.has(file)),
2719
- css: [...output.viteMetadata?.importedCss ?? []],
2720
- assets: [...output.viteMetadata?.importedAssets ?? []],
2721
- assetUrls
2722
- });
2723
- }
2724
- const referencedFiles = new Map(outputs.flatMap((output) => {
2725
- return output.type === "chunk" ? [[output.fileName, [...referencedAssets.get(output.preliminaryFileName) ?? []]]] : [];
2726
- }));
2727
- const files = new Set(outputs.map((output) => output.fileName));
2728
- for (const [key, entry] of modules) for (const file of [
2729
- entry.file,
2730
- ...entry.css,
2731
- ...entry.assets
2732
- ]) if (!files.has(file)) return yield* Effect.fail(`Client module ${key} references a missing output: ${file}`);
2733
- const frameworkEntries = /* @__PURE__ */ new Set([generation.config.clientEntryFilePath, ...viewEntries.map((entry) => entry.id)]);
2734
- const entries = /* @__PURE__ */ new Map();
2735
- for (const output of outputs) {
2736
- if (output.type !== "chunk" || !output.isEntry || output.facadeModuleId === null || !frameworkEntries.has(output.facadeModuleId)) continue;
2737
- entries.set(output.facadeModuleId, yield* Effect.fromResult(projectClientModule({
2738
- modules,
2739
- referencedFiles,
2740
- immutableFiles,
2741
- key: output.fileName,
2742
- base: generation.config.publicAssetsBasePath
2743
- })));
2744
- }
2745
- const clientEntry = entries.get(generation.config.clientEntryFilePath);
2746
- if (clientEntry === void 0) return yield* Effect.fail("Vite did not preserve the configured client entry");
2747
- const clientViews = yield* Effect.forEach(viewEntries, (view) => {
2748
- const module = entries.get(view.id);
2749
- return module === void 0 ? Effect.fail(`Vite did not preserve the view module ${view.id}`) : Effect.succeed([view.pattern, {
2750
- ...module,
2751
- exportName: view.exportName
2752
- }]);
2753
- }).pipe(Effect.map(Object.fromEntries));
2754
- return {
2755
- criticalCss,
2756
- outputs: outputs.map((output) => {
2757
- return {
2758
- fileName: output.fileName,
2759
- immutable: immutableFiles.has(output.fileName),
2760
- source: output.type === "chunk" ? output.code : typeof output.source === "string" ? output.source : Uint8Array.from(output.source)
2761
- };
2762
- }),
2763
- chunks: outputs.flatMap((output) => {
2764
- return output.type === "chunk" ? [{
2765
- fileName: output.fileName,
2766
- sourceModules: Object.keys(output.modules),
2767
- renderedLengths: Object.fromEntries(Object.entries(output.modules).map(([id, module]) => {
2768
- return [id, module.renderedLength];
2769
- })),
2770
- imports: [...output.imports],
2771
- dynamicImports: [...output.dynamicImports]
2772
- }] : [];
2773
- }),
2774
- clientEntry,
2775
- clientViews
2776
- };
2777
- }).pipe(Effect.mapError((cause) => {
2778
- return {
2779
- _tag: "ClientBundleIssue",
2780
- cause
2781
- };
2782
- }));
2783
- }
2784
- /** The entry's module and, dependencies first, every static import it reaches with their stylesheets. */
2785
- function projectClientModule(input) {
2786
- const root = input.modules.get(input.key);
2787
- if (root === void 0) return Result.fail(`Client output has no module: ${input.key}`);
2788
- const visited = /* @__PURE__ */ new Set();
2789
- const dependencies = /* @__PURE__ */ new Set();
2790
- const css = /* @__PURE__ */ new Set();
2791
- const assets = /* @__PURE__ */ new Set();
2792
- const pending = [{
2793
- key: input.key,
2794
- collect: false
2795
- }];
2796
- for (let next = pending.pop(); next !== void 0; next = pending.pop()) {
2797
- const entry = input.modules.get(next.key);
2798
- if (entry === void 0) return Result.fail(`Client output has no module: ${next.key}`);
2799
- if (!input.immutableFiles.has(entry.file)) return Result.fail(`Browser module ${entry.file} must use Vite's content-hashed output naming`);
2800
- if (next.collect) {
2801
- if (next.key !== input.key) dependencies.add(entry.file);
2802
- for (const file of entry.css) css.add(file);
2803
- for (const url of entry.assetUrls) assets.add(url);
2804
- for (const file of input.referencedFiles.get(entry.file) ?? []) assets.add(toUrl(file));
2805
- continue;
2806
- }
2807
- if (visited.has(next.key)) continue;
2808
- visited.add(next.key);
2809
- pending.push({
2810
- key: next.key,
2811
- collect: true
2812
- });
2813
- const imports = entry.imports;
2814
- for (const dependency of [...imports].reverse()) pending.push({
2815
- key: dependency,
2816
- collect: false
2817
- });
2818
- }
2819
- function toUrl(fileName) {
2820
- return input.base + fileName.split("/").map(encodeURIComponent).join("/");
2821
- }
2822
- return Result.succeed({
2823
- importUrl: toUrl(root.file),
2824
- depUrls: [...dependencies].map(toUrl),
2825
- cssBundleUrls: [...css].map(toUrl),
2826
- staticAssetUrls: [...assets]
2827
- });
2828
- }
2829
- /** Vite encodes a literal built URL's path, leaving its query and fragment intact. */
2830
- function toJavascriptAssetUrl(input) {
2831
- const { fileName, renderedUrl } = input.reference;
2832
- if (typeof renderedUrl === "object" && renderedUrl.runtime) return;
2833
- const usesRenderedUrl = typeof renderedUrl === "string" && renderedUrl.length > 0;
2834
- const url = usesRenderedUrl ? renderedUrl : fileName;
2835
- const prefix = usesRenderedUrl ? "" : input.base;
2836
- if (url.startsWith("data:")) return;
2837
- const suffixIndex = url.search(/[?#]/);
2838
- return prefix + (suffixIndex < 0 ? encodeURI(url) : encodeURI(url.slice(0, suffixIndex)) + url.slice(suffixIndex));
2839
- }
2840
- async function runClientBuild(generation, publicCss, viewEntries) {
2841
- const manifestFiles = /* @__PURE__ */ new Set();
2842
- const immutableFiles = /* @__PURE__ */ new Set();
2843
- let criticalCss = "";
2844
- let criticalCssFileName;
2845
- const captured = {};
2846
- const entries = [
2847
- {
2848
- name: "entry",
2849
- id: generation.config.clientEntryFilePath
2850
- },
2851
- ...[...new Set(viewEntries.map((entry) => entry.id))].map((id, index) => {
2852
- return {
2853
- name: `view-${index}`,
2854
- id
2855
- };
2856
- }),
2857
- ...generation.config.criticalCssEntryFilePath === void 0 ? [] : [{
2858
- name: "critical",
2859
- id: generation.config.criticalCssEntryFilePath
2860
- }]
2861
- ];
2862
- let hasDeclaration = false;
2863
- const plugin = {
2864
- name: VORMA_BUILD_PLUGIN_NAME,
2865
- api: { registerDeclarationFn: () => {
2866
- hasDeclaration = true;
2867
- } },
2868
- enforce: "post",
2869
- applyToEnvironment: (environment) => environment.name === "client",
2870
- configResolved: {
2871
- order: "post",
2872
- handler() {
2873
- if (!hasDeclaration) return this.error("Vorma's build requires vorma() from vorma/vite in the configured Vite plugins list.");
2874
- }
2875
- },
2876
- buildStart() {
2877
- for (const entry of entries) this.emitFile({
2878
- type: "chunk",
2879
- ...entry,
2880
- preserveSignature: "strict"
2881
- });
2882
- },
2883
- resolveId(source) {
2884
- return source === FRAMEWORK_IDENTITY_PROTOCOL.moduleSpecifiers.clientTransportTypes ? generation.clientTransport.filePath : null;
2885
- },
2886
- config(config) {
2887
- const workerPlugins = config.worker?.plugins;
2888
- return {
2889
- base: generation.config.publicAssetsBasePath,
2890
- publicDir: false,
2891
- resolve: { dedupe: CLIENT_SHARED_PACKAGES },
2892
- worker: { plugins: () => {
2893
- return [...workerPlugins?.() ?? [], createClientOutputHashingPlugin(immutableFiles)];
2894
- } }
2895
- };
2896
- },
2897
- configEnvironment(name, config) {
2898
- if (name !== "client") return;
2899
- const build = config.build;
2900
- const output = build?.rolldownOptions?.output;
2901
- if (Array.isArray(output)) return this.error("Vorma requires one output for its client environment");
2902
- return { build: {
2903
- outDir: generation.config.publicDirectory,
2904
- emptyOutDir: false,
2905
- write: true,
2906
- cssCodeSplit: true,
2907
- rolldownOptions: {
2908
- input: build?.rolldownOptions?.input ?? { entry: generation.config.clientEntryFilePath },
2909
- output: {
2910
- ...output,
2911
- format: "es"
2912
- }
2913
- }
2914
- } };
2915
- },
2916
- generateBundle: {
2917
- order: "post",
2918
- handler(_options, bundle) {
2919
- const config = this.environment.config;
2920
- if (config.build.watch || config.build.lib || config.build.ssr) return this.error("Vorma requires a non-watching browser client environment");
2921
- if (captured.bundle !== void 0) return this.error("Vorma requires one completed client output");
2922
- captured.bundle = bundle;
2923
- if (criticalCssFileName !== void 0) {
2924
- const stylesheet = bundle[criticalCssFileName];
2925
- if (stylesheet === void 0 || stylesheet.type !== "asset") return this.error("Vite did not preserve the compiled critical stylesheet");
2926
- criticalCss = typeof stylesheet.source === "string" ? stylesheet.source : new TextDecoder().decode(stylesheet.source);
2927
- const sourcePath = generation.config.criticalCssEntryFilePath;
2928
- const source = sourcePath === void 0 ? null : this.getModuleInfo(sourcePath);
2929
- if (!((source?.importers.length ?? 0) > 0 || (source?.dynamicImporters.length ?? 0) > 0 || Object.values(bundle).some((output) => {
2930
- return output.type === "chunk" && (output.viteMetadata?.importedCss.has(stylesheet.fileName) || output.viteMetadata?.importedAssets.has(stylesheet.fileName));
2931
- }))) delete bundle[stylesheet.fileName];
2932
- else immutableFiles.add(stylesheet.fileName);
2933
- }
2934
- for (const [setting, defaultName] of [[config.build.manifest, ".vite/manifest.json"], [config.build.ssrManifest, ".vite/ssr-manifest.json"]]) if (setting) manifestFiles.add(typeof setting === "string" ? setting : defaultName);
2935
- }
2936
- }
2937
- };
2938
- const referencedAssets = /* @__PURE__ */ new Map();
2939
- function forClient(plugin) {
2940
- return {
2941
- ...plugin,
2942
- applyToEnvironment: (environment) => environment.name === "client"
2943
- };
2944
- }
2945
- await build({
2946
- configFile: generation.config.viteConfigFilePath,
2947
- mode: "production",
2948
- plugins: [
2949
- plugin,
2950
- forClient({
2951
- name: "vorma:critical-css",
2952
- generateBundle: {
2953
- order: "pre",
2954
- handler(_options, bundle) {
2955
- const critical = findCriticalCssFileName({
2956
- sourcePath: generation.config.criticalCssEntryFilePath,
2957
- outputs: Object.values(bundle)
2958
- });
2959
- if (Result.isFailure(critical)) return this.error(critical.failure);
2960
- criticalCssFileName = critical.success;
2961
- }
2962
- }
2963
- }),
2964
- forClient(publicCss.plugin),
2965
- forClient(createClientPublicUrlPlugin({
2966
- sites: generation.inspection.publicUrlSites,
2967
- sourceTexts: generation.inspection.clientSourceTexts,
2968
- publicAssets: generation.publicAssets.assets,
2969
- publicDirectory: generation.config.publicDirectory,
2970
- referencedAssets
2971
- })),
2972
- forClient(createClientOutputHashingPlugin(immutableFiles))
2973
- ]
2974
- });
2975
- return {
2976
- result: captured.bundle === void 0 ? void 0 : { output: Object.values(captured.bundle) },
2977
- criticalCss,
2978
- manifestFiles,
2979
- immutableFiles,
2980
- referencedAssets,
2981
- cssAssets: publicCss.cssAssets,
2982
- javascriptAssets: publicCss.javascriptAssets
2983
- };
2984
- }
2985
- //#endregion
2986
- //#region src/build/index.ts
2987
- /**
2988
- * Builds and publishes one production application while holding all generated
2989
- * output ownership. `configFilePath` is the absolute path of the configuration
2990
- * file; every configured path resolves against its directory.
2991
- */
2992
- function buildApplication(input) {
2993
- return Effect.scoped(Effect.gen(function* () {
2994
- const resolved = yield* resolveVormaConfig(input).pipe(Effect.flatMap(loadApplicationSourceLayout));
2995
- yield* acquireOutputOwnership({
2996
- lockDirectory: resolved.clientGeneratedDirectory,
2997
- boundaryDirectories: [resolved.serverGeneratedDirectory],
2998
- kind: "build"
2999
- });
3000
- yield* preparePublicOutput(resolved);
3001
- const generation = yield* prepareApplicationGeneration(resolved);
3002
- yield* writeAndCheckApplicationGeneration(generation);
3003
- return yield* publishApplicationArtifacts({
3004
- generation,
3005
- artifacts: yield* assembleApplicationArtifacts({
3006
- generation,
3007
- client: yield* bundleClient(generation)
3008
- })
3009
- });
3010
- }));
3011
- }
3012
- //#endregion
3013
- export { projectBrowserContracts as C, getClientViewEntries as S, createCssPublicAssets as _, acquireOutputOwnership as a, toContentDigest as b, writeAndCheckApplicationGeneration as c, readTypescriptProjectRoots as d, runTypescriptOperation as f, containsRelativePath as g, resolveVormaConfig as h, publishApplicationArtifacts as i, collectPublicAssets as l, getGeneratedSourceDirectories as m, loadApplicationSourceLayout as n, checkOutputPath as o, withTypescriptProjects as p, preparePublicOutput as r, assembleApplicationGeneration as s, buildApplication as t, inspectProjects as u, createClientPublicUrlPlugin as v, CLIENT_SHARED_PACKAGES as x, encodeCanonicalJson as y };