@sjcx/vorma 0.0.0-pre.5 → 0.0.0-pre.7

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