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