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