@lark-apaas/fullstack-cli 1.1.60 → 1.1.61-alpha.20260818172555
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/dist/client-dependency-graph.d.ts +37 -0
- package/dist/client-dependency-graph.js +466 -0
- package/dist/index.js +3482 -143
- package/package.json +4 -1
- package/templates/scripts/cache-generation-preflight.mjs +1346 -0
- package/templates/scripts/cache-generation-prune.mjs +174 -0
- package/templates/scripts/cache-runtime-coordinator.mjs +487 -0
- package/templates/scripts/dev.js +433 -59
- package/templates/scripts/dev.sh +2 -0
- package/templates/scripts/lint.js +2 -40
- package/templates/scripts/patch-vite-dependency-graph-hash.mjs +141 -0
- package/templates/scripts/server-cache-module-resolver.cjs +63 -0
- package/templates/scripts/server-cache-runtime.mjs +681 -0
- package/templates/scripts/server-startup-runtime.mjs +496 -0
- package/templates/scripts/server-transition-runtime.mjs +655 -0
- package/templates/scripts/vite-cache-runtime.mjs +3100 -0
- package/templates/scripts/workspace-client-runtime.mjs +176 -0
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
// Canonical preflight source. Template repositories receive this file verbatim.
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
function sha256(file) {
|
|
7
|
+
return crypto
|
|
8
|
+
.createHash('sha256')
|
|
9
|
+
.update(fs.readFileSync(file))
|
|
10
|
+
.digest('hex');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readJson(file) {
|
|
14
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function runtimeAbiHashes(runtime) {
|
|
18
|
+
if (
|
|
19
|
+
typeof runtime?.runtimeAbiHash !== 'string' ||
|
|
20
|
+
runtime.runtimeAbiHash.length === 0
|
|
21
|
+
) {
|
|
22
|
+
throw new Error('MIAODA_CACHE_RUNTIME_MANIFEST_INVALID');
|
|
23
|
+
}
|
|
24
|
+
const compatible = runtime.compatibleRuntimeAbiHashes ?? [];
|
|
25
|
+
if (
|
|
26
|
+
!Array.isArray(compatible) ||
|
|
27
|
+
compatible.some(hash => typeof hash !== 'string' || hash.length === 0)
|
|
28
|
+
) {
|
|
29
|
+
throw new Error('MIAODA_CACHE_RUNTIME_MANIFEST_INVALID');
|
|
30
|
+
}
|
|
31
|
+
return new Set([runtime.runtimeAbiHash, ...compatible]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function listRegularFiles(root, cacheRoot) {
|
|
35
|
+
const files = [];
|
|
36
|
+
const visit = directory => {
|
|
37
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
38
|
+
const file = path.join(directory, entry.name);
|
|
39
|
+
if (entry.isSymbolicLink()) {
|
|
40
|
+
throw new Error(`MIAODA_CACHE_GENERATION_SYMLINK_REJECTED: ${file}`);
|
|
41
|
+
}
|
|
42
|
+
if (entry.isDirectory()) visit(file);
|
|
43
|
+
else if (entry.isFile()) files.push(path.relative(cacheRoot, file));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
visit(root);
|
|
47
|
+
return files.sort();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const WORKSPACE_SOURCE_ROOTS = ['client', 'server', 'shared', 'src'];
|
|
51
|
+
const WORKSPACE_SOURCE_FILES = [
|
|
52
|
+
'.env',
|
|
53
|
+
'.env.development',
|
|
54
|
+
'.env.development.local',
|
|
55
|
+
'drizzle.config.ts',
|
|
56
|
+
'nest-cli.json',
|
|
57
|
+
'postcss.config.js',
|
|
58
|
+
'postcss.config.cjs',
|
|
59
|
+
'postcss.config.mjs',
|
|
60
|
+
'tailwind.config.ts',
|
|
61
|
+
'tailwind.config.js',
|
|
62
|
+
'tailwind.config.cjs',
|
|
63
|
+
'tailwind.config.mjs',
|
|
64
|
+
'tsconfig.json',
|
|
65
|
+
'tsconfig.app.json',
|
|
66
|
+
'tsconfig.node.json',
|
|
67
|
+
'vite.config.ts',
|
|
68
|
+
'vite.config.mts',
|
|
69
|
+
'vite.config.js',
|
|
70
|
+
'vite.config.mjs',
|
|
71
|
+
'vite.config.cjs',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function listWorkspaceSourceFilePaths(projectRoot) {
|
|
75
|
+
const files = WORKSPACE_SOURCE_ROOTS.flatMap(relativeRoot => {
|
|
76
|
+
const root = path.join(projectRoot, relativeRoot);
|
|
77
|
+
return fs.existsSync(root) ? listRegularFiles(root, projectRoot) : [];
|
|
78
|
+
});
|
|
79
|
+
for (const relativeFile of WORKSPACE_SOURCE_FILES) {
|
|
80
|
+
const file = path.join(projectRoot, relativeFile);
|
|
81
|
+
if (!fs.existsSync(file)) continue;
|
|
82
|
+
if (fs.lstatSync(file).isSymbolicLink() || !fs.statSync(file).isFile()) {
|
|
83
|
+
throw new Error(`MIAODA_WORKSPACE_SOURCE_FILE_REJECTED: ${file}`);
|
|
84
|
+
}
|
|
85
|
+
files.push(relativeFile);
|
|
86
|
+
}
|
|
87
|
+
return [...new Set(files)].sort();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function listWorkspaceSourceFiles(projectRoot) {
|
|
91
|
+
return listWorkspaceSourceFilePaths(projectRoot).map(relativeFile => {
|
|
92
|
+
const file = path.join(projectRoot, ...relativeFile.split('/'));
|
|
93
|
+
return {
|
|
94
|
+
path: relativeFile.split(path.sep).join('/'),
|
|
95
|
+
sha256: sha256(file),
|
|
96
|
+
size: fs.statSync(file).size,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function workspaceSourceFileInScope(relativePath, artifactScope) {
|
|
102
|
+
if (artifactScope === 'all') return true;
|
|
103
|
+
if (relativePath === 'client' || relativePath.startsWith('client/')) {
|
|
104
|
+
return artifactScope === 'vite';
|
|
105
|
+
}
|
|
106
|
+
if (relativePath === 'server' || relativePath.startsWith('server/')) {
|
|
107
|
+
return artifactScope === 'server';
|
|
108
|
+
}
|
|
109
|
+
// shared/, src/ and root config files may be consumed by either build.
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function canonicalJson(value) {
|
|
114
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
115
|
+
if (value && typeof value === 'object') {
|
|
116
|
+
return Object.fromEntries(
|
|
117
|
+
Object.entries(value)
|
|
118
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
119
|
+
.map(([key, child]) => [key, canonicalJson(child)])
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function stableServerMetadataIdentity(serverMetadata) {
|
|
126
|
+
const availabilityProbe = serverMetadata?.availabilityProbe ?? {};
|
|
127
|
+
const checks = Array.isArray(availabilityProbe.checks)
|
|
128
|
+
? availabilityProbe.checks.map(check => {
|
|
129
|
+
const { elapsedMs: _elapsedMs, ...stableCheck } = check;
|
|
130
|
+
return stableCheck;
|
|
131
|
+
})
|
|
132
|
+
: availabilityProbe.checks;
|
|
133
|
+
const {
|
|
134
|
+
startupMs: _startupMs,
|
|
135
|
+
stdout: _stdout,
|
|
136
|
+
stderr: _stderr,
|
|
137
|
+
...stableProbe
|
|
138
|
+
} = availabilityProbe;
|
|
139
|
+
return canonicalJson({
|
|
140
|
+
...serverMetadata,
|
|
141
|
+
availabilityProbe: { ...stableProbe, checks },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function jsonIdentity(value) {
|
|
146
|
+
const content = JSON.stringify(value);
|
|
147
|
+
return {
|
|
148
|
+
sha256: crypto.createHash('sha256').update(content).digest('hex'),
|
|
149
|
+
size: Buffer.byteLength(content),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function serverDependenciesAreConsumable(serverDependencies) {
|
|
154
|
+
if (
|
|
155
|
+
serverDependencies?.schemaVersion !== 1 ||
|
|
156
|
+
typeof serverDependencies?.sourceEntry !== 'string' ||
|
|
157
|
+
!serverDependencies.sourceEntry ||
|
|
158
|
+
!Array.isArray(serverDependencies?.bootstrapModules) ||
|
|
159
|
+
serverDependencies.bootstrapModules.length === 0 ||
|
|
160
|
+
!serverDependencies?.transitionCompilerOptions ||
|
|
161
|
+
typeof serverDependencies.transitionCompilerOptions !== 'object' ||
|
|
162
|
+
Array.isArray(serverDependencies.transitionCompilerOptions) ||
|
|
163
|
+
!Array.isArray(serverDependencies?.packages) ||
|
|
164
|
+
serverDependencies.packages.length === 0 ||
|
|
165
|
+
!Array.isArray(serverDependencies?.actionPlugins) ||
|
|
166
|
+
!Array.isArray(serverDependencies?.files) ||
|
|
167
|
+
serverDependencies.files.length === 0 ||
|
|
168
|
+
!/^[a-f0-9]{64}$/.test(serverDependencies?.treeSha256 || '') ||
|
|
169
|
+
serverDependencies.fileCount !== serverDependencies.files.length ||
|
|
170
|
+
!Number.isSafeInteger(serverDependencies.totalBytes) ||
|
|
171
|
+
serverDependencies.totalBytes <= 0
|
|
172
|
+
) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const validPackages = packages =>
|
|
176
|
+
packages.every(
|
|
177
|
+
pkg =>
|
|
178
|
+
typeof pkg?.name === 'string' &&
|
|
179
|
+
pkg.name &&
|
|
180
|
+
typeof pkg?.version === 'string' &&
|
|
181
|
+
pkg.version
|
|
182
|
+
);
|
|
183
|
+
const paths = new Set();
|
|
184
|
+
const validFiles = serverDependencies.files.every(file => {
|
|
185
|
+
if (
|
|
186
|
+
typeof file?.path !== 'string' ||
|
|
187
|
+
!file.path ||
|
|
188
|
+
file.path !== path.posix.normalize(file.path) ||
|
|
189
|
+
file.path === '.' ||
|
|
190
|
+
file.path.startsWith('../') ||
|
|
191
|
+
path.posix.isAbsolute(file.path) ||
|
|
192
|
+
file.path.includes('\\') ||
|
|
193
|
+
paths.has(file.path) ||
|
|
194
|
+
!/^[a-f0-9]{64}$/.test(file?.sha256 || '') ||
|
|
195
|
+
!Number.isSafeInteger(file?.size) ||
|
|
196
|
+
file.size < 0
|
|
197
|
+
) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
paths.add(file.path);
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
return (
|
|
204
|
+
validPackages(serverDependencies.packages) &&
|
|
205
|
+
validPackages(serverDependencies.actionPlugins) &&
|
|
206
|
+
validFiles &&
|
|
207
|
+
crypto
|
|
208
|
+
.createHash('sha256')
|
|
209
|
+
.update(JSON.stringify(serverDependencies.files))
|
|
210
|
+
.digest('hex') === serverDependencies.treeSha256 &&
|
|
211
|
+
serverDependencies.files.reduce((total, file) => total + file.size, 0) ===
|
|
212
|
+
serverDependencies.totalBytes
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function compactServerDependencies(serverDependencies) {
|
|
217
|
+
if (!serverDependencies) return undefined;
|
|
218
|
+
const { files: _files, ...compact } = serverDependencies;
|
|
219
|
+
return compact;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function compactServerDependenciesAreConsumable(serverDependencies) {
|
|
223
|
+
return (
|
|
224
|
+
serverDependencies?.schemaVersion === 1 &&
|
|
225
|
+
typeof serverDependencies?.sourceEntry === 'string' &&
|
|
226
|
+
serverDependencies.sourceEntry.length > 0 &&
|
|
227
|
+
Array.isArray(serverDependencies?.bootstrapModules) &&
|
|
228
|
+
serverDependencies.bootstrapModules.length > 0 &&
|
|
229
|
+
serverDependencies?.transitionCompilerOptions &&
|
|
230
|
+
typeof serverDependencies.transitionCompilerOptions === 'object' &&
|
|
231
|
+
!Array.isArray(serverDependencies.transitionCompilerOptions) &&
|
|
232
|
+
Array.isArray(serverDependencies?.packages) &&
|
|
233
|
+
serverDependencies.packages.length > 0 &&
|
|
234
|
+
Array.isArray(serverDependencies?.actionPlugins) &&
|
|
235
|
+
/^[a-f0-9]{64}$/.test(serverDependencies?.treeSha256 || '') &&
|
|
236
|
+
Number.isSafeInteger(serverDependencies.fileCount) &&
|
|
237
|
+
serverDependencies.fileCount > 0 &&
|
|
238
|
+
Number.isSafeInteger(serverDependencies.totalBytes) &&
|
|
239
|
+
serverDependencies.totalBytes > 0
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function workspaceBuildInputs(projectRoot) {
|
|
244
|
+
const packageJsonFile = path.join(projectRoot, 'package.json');
|
|
245
|
+
const packageJson = fs.existsSync(packageJsonFile)
|
|
246
|
+
? readJson(packageJsonFile)
|
|
247
|
+
: {};
|
|
248
|
+
return {
|
|
249
|
+
actionPlugins: canonicalJson(packageJson.actionPlugins ?? {}),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function packageNameFromOptimizerId(id) {
|
|
254
|
+
const specifier = id.split(' > ').at(-1) || id;
|
|
255
|
+
if (
|
|
256
|
+
!specifier ||
|
|
257
|
+
specifier.startsWith('.') ||
|
|
258
|
+
specifier.startsWith('/') ||
|
|
259
|
+
specifier.startsWith('\0') ||
|
|
260
|
+
specifier.includes('://')
|
|
261
|
+
) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
const parts = specifier.split('/');
|
|
265
|
+
return specifier.startsWith('@')
|
|
266
|
+
? parts.length >= 2
|
|
267
|
+
? `${parts[0]}/${parts[1]}`
|
|
268
|
+
: undefined
|
|
269
|
+
: parts[0];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isPathInside(root, candidate) {
|
|
273
|
+
const relative = path.relative(root, candidate);
|
|
274
|
+
return (
|
|
275
|
+
relative === '' ||
|
|
276
|
+
(!relative.startsWith('..') && !path.isAbsolute(relative))
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function optimizerEntryUsesRuntimePackage({
|
|
281
|
+
projectRoot,
|
|
282
|
+
runtimeManifestFile,
|
|
283
|
+
runtime,
|
|
284
|
+
packageName,
|
|
285
|
+
entry,
|
|
286
|
+
}) {
|
|
287
|
+
const variants = runtime.packages?.[packageName];
|
|
288
|
+
if (!Array.isArray(variants) || variants.length === 0) return false;
|
|
289
|
+
if (typeof entry?.src !== 'string' || !entry.src) return false;
|
|
290
|
+
|
|
291
|
+
const source = path.isAbsolute(entry.src)
|
|
292
|
+
? entry.src
|
|
293
|
+
: path.resolve(projectRoot, entry.src);
|
|
294
|
+
if (!fs.existsSync(source)) return false;
|
|
295
|
+
const realSource = fs.realpathSync(source);
|
|
296
|
+
const runtimeRoot = path.dirname(path.resolve(runtimeManifestFile));
|
|
297
|
+
return variants.some(variant => {
|
|
298
|
+
if (typeof variant.relativePath !== 'string' || !variant.relativePath) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const packageRoot = path.resolve(runtimeRoot, variant.relativePath);
|
|
302
|
+
if (
|
|
303
|
+
!isPathInside(runtimeRoot, packageRoot) ||
|
|
304
|
+
!fs.existsSync(packageRoot)
|
|
305
|
+
) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
return isPathInside(fs.realpathSync(packageRoot), realSource);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function resolveArtifact(cacheRoot, artifact) {
|
|
313
|
+
if (
|
|
314
|
+
!artifact?.path ||
|
|
315
|
+
!artifact?.sha256 ||
|
|
316
|
+
!Number.isSafeInteger(artifact?.size) ||
|
|
317
|
+
artifact.size < 0
|
|
318
|
+
) {
|
|
319
|
+
throw new Error('MIAODA_CACHE_GENERATION_ARTIFACT_INVALID');
|
|
320
|
+
}
|
|
321
|
+
const file = path.resolve(cacheRoot, artifact.path);
|
|
322
|
+
if (file !== cacheRoot && !file.startsWith(`${cacheRoot}${path.sep}`)) {
|
|
323
|
+
throw new Error(`MIAODA_CACHE_GENERATION_PATH_ESCAPE: ${artifact.path}`);
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
!fs.existsSync(file) ||
|
|
327
|
+
fs.lstatSync(file).isSymbolicLink() ||
|
|
328
|
+
fs.statSync(file).size !== artifact.size ||
|
|
329
|
+
sha256(file) !== artifact.sha256
|
|
330
|
+
) {
|
|
331
|
+
throw new Error(`MIAODA_CACHE_GENERATION_SHA_MISMATCH: ${file}`);
|
|
332
|
+
}
|
|
333
|
+
const realFile = fs.realpathSync(file);
|
|
334
|
+
if (!isPathInside(cacheRoot, realFile)) {
|
|
335
|
+
throw new Error(`MIAODA_CACHE_GENERATION_PATH_ESCAPE: ${artifact.path}`);
|
|
336
|
+
}
|
|
337
|
+
return realFile;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function validateClientConfigFiles(projectRoot, appRuntime) {
|
|
341
|
+
const configFiles = appRuntime.clientDependencyGraph?.configFiles;
|
|
342
|
+
if (!Array.isArray(configFiles)) {
|
|
343
|
+
throw new Error('MIAODA_CACHE_CLIENT_CONFIG_FILES_INVALID');
|
|
344
|
+
}
|
|
345
|
+
const realProjectRoot = fs.realpathSync(projectRoot);
|
|
346
|
+
const sealedPaths = new Set();
|
|
347
|
+
for (const configFile of configFiles) {
|
|
348
|
+
const relativePath = configFile?.path;
|
|
349
|
+
if (
|
|
350
|
+
typeof relativePath !== 'string' ||
|
|
351
|
+
!relativePath ||
|
|
352
|
+
relativePath !== path.posix.normalize(relativePath) ||
|
|
353
|
+
relativePath === '.' ||
|
|
354
|
+
relativePath.startsWith('../') ||
|
|
355
|
+
path.posix.isAbsolute(relativePath) ||
|
|
356
|
+
relativePath.includes('\\') ||
|
|
357
|
+
sealedPaths.has(relativePath) ||
|
|
358
|
+
!/^[a-f0-9]{64}$/.test(configFile?.sha256 || '')
|
|
359
|
+
) {
|
|
360
|
+
throw new Error('MIAODA_CACHE_CLIENT_CONFIG_FILES_INVALID');
|
|
361
|
+
}
|
|
362
|
+
sealedPaths.add(relativePath);
|
|
363
|
+
const absolutePath = path.join(projectRoot, ...relativePath.split('/'));
|
|
364
|
+
let currentPath = projectRoot;
|
|
365
|
+
for (const segment of relativePath.split('/')) {
|
|
366
|
+
currentPath = path.join(currentPath, segment);
|
|
367
|
+
if (
|
|
368
|
+
!fs.existsSync(currentPath) ||
|
|
369
|
+
fs.lstatSync(currentPath).isSymbolicLink()
|
|
370
|
+
) {
|
|
371
|
+
throw new Error('MIAODA_CACHE_CLIENT_CONFIG_FILE_MISMATCH');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (!fs.statSync(absolutePath).isFile()) {
|
|
375
|
+
throw new Error('MIAODA_CACHE_CLIENT_CONFIG_FILE_MISMATCH');
|
|
376
|
+
}
|
|
377
|
+
const realFile = fs.realpathSync(absolutePath);
|
|
378
|
+
if (!isPathInside(realProjectRoot, realFile)) {
|
|
379
|
+
throw new Error('MIAODA_CACHE_CLIENT_CONFIG_FILE_MISMATCH');
|
|
380
|
+
}
|
|
381
|
+
const actualHash = crypto
|
|
382
|
+
.createHash('sha256')
|
|
383
|
+
.update(fs.readFileSync(realFile))
|
|
384
|
+
.digest('hex');
|
|
385
|
+
if (actualHash !== configFile.sha256) {
|
|
386
|
+
throw new Error('MIAODA_CACHE_CLIENT_CONFIG_FILE_MISMATCH');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const fixedNames = [
|
|
390
|
+
'vite.config.ts',
|
|
391
|
+
'vite.config.mts',
|
|
392
|
+
'vite.config.js',
|
|
393
|
+
'vite.config.mjs',
|
|
394
|
+
'vite.config.cjs',
|
|
395
|
+
'postcss.config.js',
|
|
396
|
+
'postcss.config.cjs',
|
|
397
|
+
'postcss.config.mjs',
|
|
398
|
+
'tailwind.config.ts',
|
|
399
|
+
'tailwind.config.js',
|
|
400
|
+
'tailwind.config.cjs',
|
|
401
|
+
'tailwind.config.mjs',
|
|
402
|
+
'tsconfig.json',
|
|
403
|
+
'tsconfig.app.json',
|
|
404
|
+
'tsconfig.node.json',
|
|
405
|
+
'.env',
|
|
406
|
+
'.env.development',
|
|
407
|
+
'.env.development.local',
|
|
408
|
+
];
|
|
409
|
+
const currentEntrypoints = fixedNames.filter(name =>
|
|
410
|
+
fs.existsSync(path.join(projectRoot, name))
|
|
411
|
+
);
|
|
412
|
+
const visitPatchFiles = relativeDirectory => {
|
|
413
|
+
const absoluteDirectory = path.join(projectRoot, relativeDirectory);
|
|
414
|
+
if (!fs.existsSync(absoluteDirectory)) return;
|
|
415
|
+
for (const entry of fs.readdirSync(absoluteDirectory, {
|
|
416
|
+
withFileTypes: true,
|
|
417
|
+
})) {
|
|
418
|
+
if (entry.isSymbolicLink()) {
|
|
419
|
+
throw new Error('MIAODA_CACHE_CLIENT_CONFIG_FILE_MISMATCH');
|
|
420
|
+
}
|
|
421
|
+
const child = path.posix.join(relativeDirectory, entry.name);
|
|
422
|
+
if (entry.isDirectory()) visitPatchFiles(child);
|
|
423
|
+
else if (entry.isFile()) currentEntrypoints.push(child);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
visitPatchFiles('patches');
|
|
427
|
+
visitPatchFiles('.yarn/patches');
|
|
428
|
+
if (currentEntrypoints.some(relativePath => !sealedPaths.has(relativePath))) {
|
|
429
|
+
throw new Error('MIAODA_CACHE_CLIENT_CONFIG_FILE_SET_MISMATCH');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function validateClientConfigEnvironment(appRuntime) {
|
|
434
|
+
for (const [name, expectedHash] of Object.entries(
|
|
435
|
+
appRuntime.clientDependencyGraph.configEnvironment || {}
|
|
436
|
+
)) {
|
|
437
|
+
const actualHash = crypto
|
|
438
|
+
.createHash('sha256')
|
|
439
|
+
.update(process.env[name] || '')
|
|
440
|
+
.digest('hex');
|
|
441
|
+
if (actualHash !== expectedHash) {
|
|
442
|
+
throw new Error(
|
|
443
|
+
`MIAODA_CACHE_CLIENT_CONFIG_ENVIRONMENT_MISMATCH: ${name}`
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const VALIDATION_RECEIPT_SCHEMA_VERSION = 3;
|
|
450
|
+
const VALIDATION_RECEIPT_MAX_BYTES = 1024 * 1024;
|
|
451
|
+
const VALIDATION_RECEIPT_MAX_AGE_MS = 5 * 60 * 1000;
|
|
452
|
+
|
|
453
|
+
function statIdentity(file) {
|
|
454
|
+
const stat = fs.lstatSync(file, { bigint: true });
|
|
455
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
456
|
+
throw new Error(`MIAODA_CACHE_VALIDATION_RECEIPT_FILE_REJECTED: ${file}`);
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
dev: String(stat.dev),
|
|
460
|
+
ino: String(stat.ino),
|
|
461
|
+
mode: String(stat.mode),
|
|
462
|
+
size: String(stat.size),
|
|
463
|
+
mtimeNs: String(stat.mtimeNs),
|
|
464
|
+
ctimeNs: String(stat.ctimeNs),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function receiptFileDescriptor(
|
|
469
|
+
{ projectRoot, cacheRoot, runtimeManifestFile },
|
|
470
|
+
file
|
|
471
|
+
) {
|
|
472
|
+
const resolvedFile = path.resolve(file);
|
|
473
|
+
if (isPathInside(cacheRoot, resolvedFile)) {
|
|
474
|
+
return { root: 'cache', path: path.relative(cacheRoot, resolvedFile) };
|
|
475
|
+
}
|
|
476
|
+
if (isPathInside(projectRoot, resolvedFile)) {
|
|
477
|
+
return { root: 'project', path: path.relative(projectRoot, resolvedFile) };
|
|
478
|
+
}
|
|
479
|
+
if (resolvedFile === runtimeManifestFile) {
|
|
480
|
+
return { root: 'runtime-manifest', path: '' };
|
|
481
|
+
}
|
|
482
|
+
throw new Error(`MIAODA_CACHE_VALIDATION_RECEIPT_PATH_REJECTED: ${file}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function resolveReceiptFile(
|
|
486
|
+
{ projectRoot, cacheRoot, runtimeManifestFile },
|
|
487
|
+
descriptor
|
|
488
|
+
) {
|
|
489
|
+
let root;
|
|
490
|
+
if (descriptor?.root === 'cache') root = cacheRoot;
|
|
491
|
+
else if (descriptor?.root === 'project') root = projectRoot;
|
|
492
|
+
else if (descriptor?.root === 'runtime-manifest') {
|
|
493
|
+
if (descriptor.path !== '') {
|
|
494
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
495
|
+
}
|
|
496
|
+
return runtimeManifestFile;
|
|
497
|
+
} else {
|
|
498
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
499
|
+
}
|
|
500
|
+
if (
|
|
501
|
+
typeof descriptor.path !== 'string' ||
|
|
502
|
+
descriptor.path !== path.normalize(descriptor.path) ||
|
|
503
|
+
descriptor.path === '..' ||
|
|
504
|
+
descriptor.path.startsWith(`..${path.sep}`) ||
|
|
505
|
+
path.isAbsolute(descriptor.path)
|
|
506
|
+
) {
|
|
507
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
508
|
+
}
|
|
509
|
+
const file = path.resolve(root, descriptor.path);
|
|
510
|
+
if (!isPathInside(root, file)) {
|
|
511
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
512
|
+
}
|
|
513
|
+
return file;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function receiptId(receiptWithoutId) {
|
|
517
|
+
return crypto
|
|
518
|
+
.createHash('sha256')
|
|
519
|
+
.update(JSON.stringify(canonicalJson(receiptWithoutId)))
|
|
520
|
+
.digest('hex');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function compactReceiptGeneration(generation) {
|
|
524
|
+
return {
|
|
525
|
+
schemaVersion: generation.schemaVersion,
|
|
526
|
+
generationId: generation.generationId,
|
|
527
|
+
runtimeAbiHash: generation.runtimeAbiHash,
|
|
528
|
+
applicationKind: generation.applicationKind,
|
|
529
|
+
serverRuntime: generation.serverRuntime,
|
|
530
|
+
serverRuntimeMode: generation.serverRuntimeMode,
|
|
531
|
+
artifacts: generation.artifacts,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function resolveReceiptCacheArtifact(cacheRoot, artifact) {
|
|
536
|
+
if (typeof artifact?.path !== 'string' || !artifact.path) {
|
|
537
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
538
|
+
}
|
|
539
|
+
const file = path.resolve(cacheRoot, artifact.path);
|
|
540
|
+
if (!isPathInside(cacheRoot, file)) {
|
|
541
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_PATH_REJECTED');
|
|
542
|
+
}
|
|
543
|
+
const realFile = fs.realpathSync(file);
|
|
544
|
+
if (!isPathInside(cacheRoot, realFile)) {
|
|
545
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_PATH_REJECTED');
|
|
546
|
+
}
|
|
547
|
+
return realFile;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export function writeCacheGenerationValidationReceipt({
|
|
551
|
+
validated,
|
|
552
|
+
receiptFile,
|
|
553
|
+
}) {
|
|
554
|
+
const artifactScope = validated?.validation?.artifactScope;
|
|
555
|
+
if (
|
|
556
|
+
!['all', 'server', 'vite'].includes(artifactScope) ||
|
|
557
|
+
!validated?.generation?.generationId
|
|
558
|
+
) {
|
|
559
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_SCOPE_REJECTED');
|
|
560
|
+
}
|
|
561
|
+
const roots = {
|
|
562
|
+
projectRoot: fs.realpathSync(validated.projectRoot),
|
|
563
|
+
cacheRoot: fs.realpathSync(validated.cacheRoot),
|
|
564
|
+
runtimeManifestFile: fs.realpathSync(validated.runtimeManifestFile),
|
|
565
|
+
};
|
|
566
|
+
const controlFiles = new Map();
|
|
567
|
+
const addControlFile = file => {
|
|
568
|
+
const resolvedFile = path.resolve(file);
|
|
569
|
+
controlFiles.set(resolvedFile, true);
|
|
570
|
+
};
|
|
571
|
+
addControlFile(validated.generationFile);
|
|
572
|
+
addControlFile(roots.runtimeManifestFile);
|
|
573
|
+
for (const file of Object.values(validated.artifacts)) addControlFile(file);
|
|
574
|
+
const controls = Array.from(controlFiles.keys())
|
|
575
|
+
.sort((left, right) => left.localeCompare(right))
|
|
576
|
+
.map(file => ({
|
|
577
|
+
...receiptFileDescriptor(roots, file),
|
|
578
|
+
stat: statIdentity(file),
|
|
579
|
+
}));
|
|
580
|
+
const receiptWithoutId = {
|
|
581
|
+
schemaVersion: VALIDATION_RECEIPT_SCHEMA_VERSION,
|
|
582
|
+
validatedAtMs: Date.now(),
|
|
583
|
+
artifactScope,
|
|
584
|
+
roots,
|
|
585
|
+
generationId: validated.generation.generationId,
|
|
586
|
+
controls,
|
|
587
|
+
validated: {
|
|
588
|
+
generation: compactReceiptGeneration(validated.generation),
|
|
589
|
+
runtime: validated.runtime,
|
|
590
|
+
serverMetadata: validated.serverMetadata,
|
|
591
|
+
serverDependencies: compactServerDependencies(
|
|
592
|
+
validated.serverDependencies
|
|
593
|
+
),
|
|
594
|
+
appRuntime: validated.appRuntime,
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
const receipt = {
|
|
598
|
+
...receiptWithoutId,
|
|
599
|
+
receiptId: receiptId(receiptWithoutId),
|
|
600
|
+
};
|
|
601
|
+
const resolvedReceiptFile = path.resolve(receiptFile);
|
|
602
|
+
fs.mkdirSync(path.dirname(resolvedReceiptFile), { recursive: true });
|
|
603
|
+
const temporaryFile = `${resolvedReceiptFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
604
|
+
try {
|
|
605
|
+
fs.writeFileSync(temporaryFile, `${JSON.stringify(receipt)}\n`, {
|
|
606
|
+
mode: 0o600,
|
|
607
|
+
flag: 'wx',
|
|
608
|
+
});
|
|
609
|
+
fs.renameSync(temporaryFile, resolvedReceiptFile);
|
|
610
|
+
} finally {
|
|
611
|
+
fs.rmSync(temporaryFile, { force: true });
|
|
612
|
+
}
|
|
613
|
+
return receipt;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// The image-owned preflight hashes every artifact and source once. Serving
|
|
617
|
+
// runtimes consume this short-lived receipt without walking the workspace or
|
|
618
|
+
// sealed dependency tree again. Only the generation control plane (manifest,
|
|
619
|
+
// runtime ABI metadata and named artifact descriptors) is identity-checked.
|
|
620
|
+
// Publication of a new generation, control-file mutation, or stale/malformed
|
|
621
|
+
// receipt is a cache miss and callers fall back to the original SHA validation.
|
|
622
|
+
export function validateCacheGenerationReceipt({
|
|
623
|
+
projectRoot,
|
|
624
|
+
cacheRoot,
|
|
625
|
+
runtimeManifestFile,
|
|
626
|
+
receiptFile,
|
|
627
|
+
validateConfigEnvironment = true,
|
|
628
|
+
artifactScope = 'all',
|
|
629
|
+
}) {
|
|
630
|
+
if (!['all', 'server', 'vite'].includes(artifactScope)) {
|
|
631
|
+
throw new Error('MIAODA_CACHE_VALIDATION_SCOPE_INVALID');
|
|
632
|
+
}
|
|
633
|
+
const roots = {
|
|
634
|
+
projectRoot: fs.realpathSync(path.resolve(projectRoot)),
|
|
635
|
+
cacheRoot: fs.realpathSync(path.resolve(cacheRoot)),
|
|
636
|
+
runtimeManifestFile: fs.realpathSync(path.resolve(runtimeManifestFile)),
|
|
637
|
+
};
|
|
638
|
+
const resolvedReceiptFile = path.resolve(receiptFile);
|
|
639
|
+
const receiptStat = fs.lstatSync(resolvedReceiptFile);
|
|
640
|
+
if (
|
|
641
|
+
receiptStat.isSymbolicLink() ||
|
|
642
|
+
!receiptStat.isFile() ||
|
|
643
|
+
(receiptStat.mode & 0o022) !== 0 ||
|
|
644
|
+
receiptStat.size > VALIDATION_RECEIPT_MAX_BYTES
|
|
645
|
+
) {
|
|
646
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
647
|
+
}
|
|
648
|
+
const receipt = readJson(resolvedReceiptFile);
|
|
649
|
+
const { receiptId: actualReceiptId, ...receiptWithoutId } = receipt;
|
|
650
|
+
// Receipts produced before scoped validation existed covered the full
|
|
651
|
+
// generation. A scoped receipt may only be consumed by the same artifact
|
|
652
|
+
// runtime; otherwise a server-only check could accidentally authorize Vite
|
|
653
|
+
// files (or vice versa) that were never validated.
|
|
654
|
+
const receiptArtifactScope = receipt.artifactScope || 'all';
|
|
655
|
+
const receiptCoversRequestedScope =
|
|
656
|
+
receiptArtifactScope === 'all' || receiptArtifactScope === artifactScope;
|
|
657
|
+
if (
|
|
658
|
+
receipt.schemaVersion !== VALIDATION_RECEIPT_SCHEMA_VERSION ||
|
|
659
|
+
!/^[a-f0-9]{64}$/.test(actualReceiptId || '') ||
|
|
660
|
+
receiptId(receiptWithoutId) !== actualReceiptId ||
|
|
661
|
+
!Number.isSafeInteger(receipt.validatedAtMs) ||
|
|
662
|
+
receipt.validatedAtMs > Date.now() + 5_000 ||
|
|
663
|
+
Date.now() - receipt.validatedAtMs > VALIDATION_RECEIPT_MAX_AGE_MS ||
|
|
664
|
+
JSON.stringify(receipt.roots) !== JSON.stringify(roots) ||
|
|
665
|
+
!['all', 'server', 'vite'].includes(receiptArtifactScope) ||
|
|
666
|
+
!receiptCoversRequestedScope
|
|
667
|
+
) {
|
|
668
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
669
|
+
}
|
|
670
|
+
const {
|
|
671
|
+
generation,
|
|
672
|
+
runtime,
|
|
673
|
+
serverMetadata,
|
|
674
|
+
serverDependencies,
|
|
675
|
+
appRuntime,
|
|
676
|
+
} = receipt.validated || {};
|
|
677
|
+
const applicationKind =
|
|
678
|
+
generation?.applicationKind === 'frontend-only'
|
|
679
|
+
? 'frontend-only'
|
|
680
|
+
: 'fullstack';
|
|
681
|
+
if (
|
|
682
|
+
![3, 4].includes(generation?.schemaVersion) ||
|
|
683
|
+
generation.generationId !== receipt.generationId ||
|
|
684
|
+
!runtimeAbiHashes(runtime).has(generation.runtimeAbiHash)
|
|
685
|
+
) {
|
|
686
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_MISMATCH');
|
|
687
|
+
}
|
|
688
|
+
if (artifactScope === 'server' && applicationKind === 'frontend-only') {
|
|
689
|
+
throw new Error('MIAODA_CACHE_SERVER_ARTIFACT_NOT_AVAILABLE');
|
|
690
|
+
}
|
|
691
|
+
if (
|
|
692
|
+
/^[a-f0-9]{64}$/.test(path.basename(roots.cacheRoot)) &&
|
|
693
|
+
path.basename(roots.cacheRoot) !== generation.generationId
|
|
694
|
+
) {
|
|
695
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_MISMATCH');
|
|
696
|
+
}
|
|
697
|
+
if (
|
|
698
|
+
!Array.isArray(receipt.controls) ||
|
|
699
|
+
receipt.controls.length === 0 ||
|
|
700
|
+
receipt.controls.length > 64
|
|
701
|
+
) {
|
|
702
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
703
|
+
}
|
|
704
|
+
for (const descriptor of receipt.controls) {
|
|
705
|
+
const file = resolveReceiptFile(roots, descriptor);
|
|
706
|
+
if (
|
|
707
|
+
JSON.stringify(statIdentity(file)) !== JSON.stringify(descriptor.stat)
|
|
708
|
+
) {
|
|
709
|
+
throw new Error(`MIAODA_CACHE_VALIDATION_RECEIPT_FILE_CHANGED: ${file}`);
|
|
710
|
+
}
|
|
711
|
+
const realFile = fs.realpathSync(file);
|
|
712
|
+
const expectedRoot =
|
|
713
|
+
descriptor.root === 'cache'
|
|
714
|
+
? roots.cacheRoot
|
|
715
|
+
: descriptor.root === 'project'
|
|
716
|
+
? roots.projectRoot
|
|
717
|
+
: path.dirname(roots.runtimeManifestFile);
|
|
718
|
+
if (
|
|
719
|
+
descriptor.root !== 'runtime-manifest' &&
|
|
720
|
+
!isPathInside(expectedRoot, realFile)
|
|
721
|
+
) {
|
|
722
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_PATH_REJECTED');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (artifactScope !== 'vite' && applicationKind === 'fullstack') {
|
|
726
|
+
const expectedRuntime = generation.serverRuntime || {};
|
|
727
|
+
if (
|
|
728
|
+
String(expectedRuntime.nodeModuleAbi) !== process.versions.modules ||
|
|
729
|
+
expectedRuntime.platform !== process.platform ||
|
|
730
|
+
expectedRuntime.arch !== process.arch ||
|
|
731
|
+
String(expectedRuntime.nodeVersion || '').replace(/^v/, '') !==
|
|
732
|
+
process.versions.node
|
|
733
|
+
) {
|
|
734
|
+
throw new Error('MIAODA_SERVER_CACHE_ABI_MISMATCH');
|
|
735
|
+
}
|
|
736
|
+
if (
|
|
737
|
+
generation.schemaVersion === 4 &&
|
|
738
|
+
(generation.serverRuntimeMode !== 'workspace-source-dependencies' ||
|
|
739
|
+
!compactServerDependenciesAreConsumable(serverDependencies))
|
|
740
|
+
) {
|
|
741
|
+
throw new Error('MIAODA_SERVER_RUNTIME_DEPENDENCIES_REJECTED');
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
if (artifactScope !== 'server') {
|
|
745
|
+
if (
|
|
746
|
+
appRuntime?.schemaVersion !== 1 ||
|
|
747
|
+
appRuntime.runtimeAbiHash !== generation.runtimeAbiHash
|
|
748
|
+
) {
|
|
749
|
+
throw new Error('MIAODA_APP_RUNTIME_ABI_MISMATCH');
|
|
750
|
+
}
|
|
751
|
+
if (validateConfigEnvironment) validateClientConfigEnvironment(appRuntime);
|
|
752
|
+
}
|
|
753
|
+
const shouldResolveArtifact = name =>
|
|
754
|
+
(applicationKind === 'fullstack' &&
|
|
755
|
+
artifactScope !== 'vite' &&
|
|
756
|
+
(generation.schemaVersion === 4
|
|
757
|
+
? name === 'serverDependencies'
|
|
758
|
+
: name === 'serverBundle' ||
|
|
759
|
+
name === 'serverMetadata' ||
|
|
760
|
+
name.startsWith('serverRuntimeAsset:'))) ||
|
|
761
|
+
(artifactScope !== 'server' &&
|
|
762
|
+
(name === 'appRuntime' || name === 'viteMetadata'));
|
|
763
|
+
const artifacts = Object.fromEntries(
|
|
764
|
+
Object.entries(generation.artifacts || {})
|
|
765
|
+
.filter(([name]) => shouldResolveArtifact(name))
|
|
766
|
+
.map(([name, artifact]) => [
|
|
767
|
+
name,
|
|
768
|
+
resolveReceiptCacheArtifact(roots.cacheRoot, artifact),
|
|
769
|
+
])
|
|
770
|
+
);
|
|
771
|
+
if (
|
|
772
|
+
generation.schemaVersion === 4 &&
|
|
773
|
+
applicationKind === 'fullstack' &&
|
|
774
|
+
artifactScope !== 'vite'
|
|
775
|
+
) {
|
|
776
|
+
const transitionArtifacts = {
|
|
777
|
+
serverBundle: path.join(
|
|
778
|
+
roots.cacheRoot,
|
|
779
|
+
'server',
|
|
780
|
+
'bundle',
|
|
781
|
+
'server.bundle.cjs'
|
|
782
|
+
),
|
|
783
|
+
serverMetadata: path.join(
|
|
784
|
+
roots.cacheRoot,
|
|
785
|
+
'server',
|
|
786
|
+
'bundle',
|
|
787
|
+
'server.bundle.cjs.meta.json'
|
|
788
|
+
),
|
|
789
|
+
};
|
|
790
|
+
const receiptControls = new Set(
|
|
791
|
+
receipt.controls
|
|
792
|
+
.filter(descriptor => descriptor?.root === 'cache')
|
|
793
|
+
.map(descriptor => path.resolve(roots.cacheRoot, descriptor.path))
|
|
794
|
+
);
|
|
795
|
+
const exists = Object.values(transitionArtifacts).map(file =>
|
|
796
|
+
fs.existsSync(file)
|
|
797
|
+
);
|
|
798
|
+
if (exists.some(Boolean) && !exists.every(Boolean)) {
|
|
799
|
+
throw new Error('MIAODA_SERVER_TRANSITION_BUNDLE_INCOMPLETE');
|
|
800
|
+
}
|
|
801
|
+
if (exists.every(Boolean)) {
|
|
802
|
+
for (const [name, file] of Object.entries(transitionArtifacts)) {
|
|
803
|
+
if (!receiptControls.has(file)) {
|
|
804
|
+
throw new Error('MIAODA_CACHE_VALIDATION_RECEIPT_INVALID');
|
|
805
|
+
}
|
|
806
|
+
artifacts[name] = fs.realpathSync(file);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
projectRoot: roots.projectRoot,
|
|
812
|
+
cacheRoot: roots.cacheRoot,
|
|
813
|
+
runtimeManifestFile: roots.runtimeManifestFile,
|
|
814
|
+
generationFile: path.join(roots.cacheRoot, 'generation.json'),
|
|
815
|
+
generation,
|
|
816
|
+
runtime,
|
|
817
|
+
artifacts,
|
|
818
|
+
serverMetadata:
|
|
819
|
+
artifactScope === 'vite' || applicationKind === 'frontend-only'
|
|
820
|
+
? undefined
|
|
821
|
+
: serverMetadata,
|
|
822
|
+
serverDependencies:
|
|
823
|
+
artifactScope === 'vite' || applicationKind === 'frontend-only'
|
|
824
|
+
? undefined
|
|
825
|
+
: serverDependencies,
|
|
826
|
+
appRuntime: artifactScope === 'server' ? undefined : appRuntime,
|
|
827
|
+
// The serving runtime copies the sealed Vite directory as a whole and
|
|
828
|
+
// never consumes the per-file list. Returning an empty list keeps receipt
|
|
829
|
+
// consumption independent of projects with thousands of optimized files.
|
|
830
|
+
viteFiles: [],
|
|
831
|
+
validation: {
|
|
832
|
+
artifactScope,
|
|
833
|
+
validateConfigEnvironment,
|
|
834
|
+
validatedBy: 'platform-receipt',
|
|
835
|
+
},
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export function validateCacheGeneration({
|
|
840
|
+
projectRoot,
|
|
841
|
+
cacheRoot,
|
|
842
|
+
runtimeManifestFile,
|
|
843
|
+
validateConfigEnvironment = true,
|
|
844
|
+
artifactScope = 'all',
|
|
845
|
+
validateWorkspaceInputs = true,
|
|
846
|
+
}) {
|
|
847
|
+
if (!['all', 'server', 'vite'].includes(artifactScope)) {
|
|
848
|
+
throw new Error('MIAODA_CACHE_VALIDATION_SCOPE_INVALID');
|
|
849
|
+
}
|
|
850
|
+
if (!validateWorkspaceInputs && artifactScope !== 'vite') {
|
|
851
|
+
throw new Error('MIAODA_CACHE_WORKSPACE_INPUT_SKIP_REQUIRES_VITE_SCOPE');
|
|
852
|
+
}
|
|
853
|
+
const requestedServerArtifacts =
|
|
854
|
+
artifactScope === 'all' || artifactScope === 'server';
|
|
855
|
+
const validateViteArtifacts =
|
|
856
|
+
artifactScope === 'all' || artifactScope === 'vite';
|
|
857
|
+
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
858
|
+
const realProjectRoot = fs.realpathSync(resolvedProjectRoot);
|
|
859
|
+
const requestedCacheRoot = path.resolve(
|
|
860
|
+
cacheRoot || path.join(resolvedProjectRoot, '.miaoda-cache', 'current')
|
|
861
|
+
);
|
|
862
|
+
const resolvedCacheRoot = fs.existsSync(requestedCacheRoot)
|
|
863
|
+
? fs.realpathSync(requestedCacheRoot)
|
|
864
|
+
: requestedCacheRoot;
|
|
865
|
+
const cacheRelative = path.relative(realProjectRoot, resolvedCacheRoot);
|
|
866
|
+
if (
|
|
867
|
+
cacheRelative === '..' ||
|
|
868
|
+
cacheRelative.startsWith(`..${path.sep}`) ||
|
|
869
|
+
path.isAbsolute(cacheRelative)
|
|
870
|
+
) {
|
|
871
|
+
throw new Error('MIAODA_CACHE_GENERATION_ROOT_ESCAPE');
|
|
872
|
+
}
|
|
873
|
+
const resolvedRuntimeManifest = path.resolve(runtimeManifestFile);
|
|
874
|
+
const generationFile = path.join(resolvedCacheRoot, 'generation.json');
|
|
875
|
+
for (const file of [generationFile, resolvedRuntimeManifest]) {
|
|
876
|
+
if (!fs.existsSync(file)) {
|
|
877
|
+
throw new Error(`MIAODA_CACHE_BOOTSTRAP_INPUT_MISSING: ${file}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const generation = readJson(generationFile);
|
|
881
|
+
const runtime = readJson(resolvedRuntimeManifest);
|
|
882
|
+
if (![3, 4].includes(generation.schemaVersion) || !generation.generationId) {
|
|
883
|
+
throw new Error('MIAODA_CACHE_GENERATION_SCHEMA_UNSUPPORTED');
|
|
884
|
+
}
|
|
885
|
+
if (
|
|
886
|
+
generation.applicationKind !== undefined &&
|
|
887
|
+
!['fullstack', 'frontend-only'].includes(generation.applicationKind)
|
|
888
|
+
) {
|
|
889
|
+
throw new Error('MIAODA_CACHE_APPLICATION_KIND_INVALID');
|
|
890
|
+
}
|
|
891
|
+
const applicationKind =
|
|
892
|
+
generation.applicationKind === 'frontend-only'
|
|
893
|
+
? 'frontend-only'
|
|
894
|
+
: 'fullstack';
|
|
895
|
+
if (artifactScope === 'server' && applicationKind === 'frontend-only') {
|
|
896
|
+
throw new Error('MIAODA_CACHE_SERVER_ARTIFACT_NOT_AVAILABLE');
|
|
897
|
+
}
|
|
898
|
+
const validateServerArtifacts =
|
|
899
|
+
requestedServerArtifacts && applicationKind === 'fullstack';
|
|
900
|
+
if (
|
|
901
|
+
validateServerArtifacts &&
|
|
902
|
+
generation.schemaVersion === 3 &&
|
|
903
|
+
(!/^[a-f0-9]{64}$/.test(generation.serverMetadataIdentity?.sha256 || '') ||
|
|
904
|
+
!Number.isSafeInteger(generation.serverMetadataIdentity?.size) ||
|
|
905
|
+
generation.serverMetadataIdentity.size < 0)
|
|
906
|
+
) {
|
|
907
|
+
throw new Error('MIAODA_CACHE_SERVER_METADATA_IDENTITY_INVALID');
|
|
908
|
+
}
|
|
909
|
+
const { generationId, ...generationIdentity } = generation;
|
|
910
|
+
const generationIdIdentity =
|
|
911
|
+
applicationKind === 'fullstack' && generation.schemaVersion === 3
|
|
912
|
+
? {
|
|
913
|
+
...generationIdentity,
|
|
914
|
+
artifacts: {
|
|
915
|
+
...generationIdentity.artifacts,
|
|
916
|
+
serverMetadata: {
|
|
917
|
+
...generationIdentity.artifacts?.serverMetadata,
|
|
918
|
+
...generation.serverMetadataIdentity,
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
}
|
|
922
|
+
: generationIdentity;
|
|
923
|
+
const expectedGenerationId = crypto
|
|
924
|
+
.createHash('sha256')
|
|
925
|
+
.update(JSON.stringify(generationIdIdentity))
|
|
926
|
+
.digest('hex');
|
|
927
|
+
if (generationId !== expectedGenerationId) {
|
|
928
|
+
throw new Error('MIAODA_CACHE_GENERATION_ID_MISMATCH');
|
|
929
|
+
}
|
|
930
|
+
if (
|
|
931
|
+
runtime.schemaVersion !== 1 ||
|
|
932
|
+
!runtimeAbiHashes(runtime).has(generation.runtimeAbiHash)
|
|
933
|
+
) {
|
|
934
|
+
throw new Error('MIAODA_CACHE_RUNTIME_ABI_MISMATCH');
|
|
935
|
+
}
|
|
936
|
+
const shouldResolveArtifact = name =>
|
|
937
|
+
(validateServerArtifacts &&
|
|
938
|
+
(generation.schemaVersion === 4
|
|
939
|
+
? name === 'serverDependencies'
|
|
940
|
+
: name === 'serverBundle' ||
|
|
941
|
+
name === 'serverMetadata' ||
|
|
942
|
+
name.startsWith('serverRuntimeAsset:'))) ||
|
|
943
|
+
(validateViteArtifacts &&
|
|
944
|
+
(name === 'appRuntime' || name === 'viteMetadata'));
|
|
945
|
+
const artifacts = Object.fromEntries(
|
|
946
|
+
Object.entries(generation.artifacts || {})
|
|
947
|
+
.filter(([name]) => shouldResolveArtifact(name))
|
|
948
|
+
.map(([name, artifact]) => [
|
|
949
|
+
name,
|
|
950
|
+
resolveArtifact(resolvedCacheRoot, artifact),
|
|
951
|
+
])
|
|
952
|
+
);
|
|
953
|
+
const requiredArtifacts = [
|
|
954
|
+
...(validateServerArtifacts
|
|
955
|
+
? generation.schemaVersion === 4
|
|
956
|
+
? ['serverDependencies']
|
|
957
|
+
: ['serverBundle', 'serverMetadata']
|
|
958
|
+
: []),
|
|
959
|
+
...(validateViteArtifacts ? ['appRuntime', 'viteMetadata'] : []),
|
|
960
|
+
];
|
|
961
|
+
for (const name of requiredArtifacts) {
|
|
962
|
+
if (!artifacts[name]) {
|
|
963
|
+
throw new Error(`MIAODA_CACHE_GENERATION_ARTIFACT_MISSING: ${name}`);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (
|
|
967
|
+
(validateViteArtifacts && !Array.isArray(generation.viteFiles)) ||
|
|
968
|
+
(validateViteArtifacts && generation.viteFiles.length === 0)
|
|
969
|
+
) {
|
|
970
|
+
throw new Error('MIAODA_CACHE_GENERATION_VITE_FILES_MISSING');
|
|
971
|
+
}
|
|
972
|
+
if (
|
|
973
|
+
generation.schemaVersion === 3 &&
|
|
974
|
+
!Array.isArray(generation.sourceFiles)
|
|
975
|
+
) {
|
|
976
|
+
throw new Error('MIAODA_CACHE_GENERATION_SOURCE_FILES_MISSING');
|
|
977
|
+
}
|
|
978
|
+
if (validateWorkspaceInputs && generation.schemaVersion === 3) {
|
|
979
|
+
const currentSourceFiles = listWorkspaceSourceFiles(
|
|
980
|
+
resolvedProjectRoot
|
|
981
|
+
).filter(file => workspaceSourceFileInScope(file.path, artifactScope));
|
|
982
|
+
const sealedSourceFiles = generation.sourceFiles.filter(file =>
|
|
983
|
+
workspaceSourceFileInScope(file.path, artifactScope)
|
|
984
|
+
);
|
|
985
|
+
if (
|
|
986
|
+
JSON.stringify(currentSourceFiles) !== JSON.stringify(sealedSourceFiles)
|
|
987
|
+
) {
|
|
988
|
+
throw new Error('MIAODA_CACHE_GENERATION_SOURCE_FILE_SET_MISMATCH');
|
|
989
|
+
}
|
|
990
|
+
if (
|
|
991
|
+
JSON.stringify(workspaceBuildInputs(resolvedProjectRoot)) !==
|
|
992
|
+
JSON.stringify(generation.workspaceBuildInputs)
|
|
993
|
+
) {
|
|
994
|
+
throw new Error('MIAODA_CACHE_GENERATION_BUILD_INPUT_MISMATCH');
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
let viteFiles = [];
|
|
998
|
+
let serverMetadata;
|
|
999
|
+
let serverDependencies;
|
|
1000
|
+
let appRuntime;
|
|
1001
|
+
let viteMetadata;
|
|
1002
|
+
if (validateViteArtifacts) {
|
|
1003
|
+
const viteFilePaths = new Set();
|
|
1004
|
+
viteFiles = generation.viteFiles.map(artifact => {
|
|
1005
|
+
if (viteFilePaths.has(artifact?.path)) {
|
|
1006
|
+
throw new Error(
|
|
1007
|
+
`MIAODA_CACHE_GENERATION_VITE_FILE_DUPLICATE: ${artifact?.path}`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
viteFilePaths.add(artifact?.path);
|
|
1011
|
+
return resolveArtifact(resolvedCacheRoot, artifact);
|
|
1012
|
+
});
|
|
1013
|
+
if (!viteFilePaths.has(generation.artifacts.viteMetadata.path)) {
|
|
1014
|
+
throw new Error('MIAODA_CACHE_GENERATION_VITE_METADATA_UNSEALED');
|
|
1015
|
+
}
|
|
1016
|
+
const actualViteFiles = listRegularFiles(
|
|
1017
|
+
path.join(resolvedCacheRoot, 'vite'),
|
|
1018
|
+
resolvedCacheRoot
|
|
1019
|
+
);
|
|
1020
|
+
const sealedViteFiles = Array.from(viteFilePaths).sort();
|
|
1021
|
+
if (JSON.stringify(actualViteFiles) !== JSON.stringify(sealedViteFiles)) {
|
|
1022
|
+
throw new Error('MIAODA_CACHE_GENERATION_VITE_FILE_SET_MISMATCH');
|
|
1023
|
+
}
|
|
1024
|
+
appRuntime = readJson(artifacts.appRuntime);
|
|
1025
|
+
viteMetadata = readJson(artifacts.viteMetadata);
|
|
1026
|
+
}
|
|
1027
|
+
if (validateServerArtifacts && generation.schemaVersion === 4) {
|
|
1028
|
+
serverDependencies = readJson(artifacts.serverDependencies);
|
|
1029
|
+
if (!serverDependenciesAreConsumable(serverDependencies)) {
|
|
1030
|
+
throw new Error('MIAODA_SERVER_RUNTIME_DEPENDENCIES_REJECTED');
|
|
1031
|
+
}
|
|
1032
|
+
if (generation.serverRuntimeMode !== 'workspace-source-dependencies') {
|
|
1033
|
+
throw new Error('MIAODA_SERVER_RUNTIME_MODE_REJECTED');
|
|
1034
|
+
}
|
|
1035
|
+
if (
|
|
1036
|
+
!Array.isArray(generation.serverFiles) ||
|
|
1037
|
+
generation.serverFiles.length < 2
|
|
1038
|
+
) {
|
|
1039
|
+
throw new Error('MIAODA_SERVER_RUNTIME_FILES_MISSING');
|
|
1040
|
+
}
|
|
1041
|
+
const sealedServerFiles = generation.serverFiles.map(artifact =>
|
|
1042
|
+
resolveArtifact(resolvedCacheRoot, artifact)
|
|
1043
|
+
);
|
|
1044
|
+
const actualServerFiles = listRegularFiles(
|
|
1045
|
+
path.join(resolvedCacheRoot, 'server'),
|
|
1046
|
+
resolvedCacheRoot
|
|
1047
|
+
);
|
|
1048
|
+
const sealedServerPaths = generation.serverFiles
|
|
1049
|
+
.map(artifact => artifact?.path)
|
|
1050
|
+
.sort();
|
|
1051
|
+
if (
|
|
1052
|
+
JSON.stringify(actualServerFiles) !== JSON.stringify(sealedServerPaths)
|
|
1053
|
+
) {
|
|
1054
|
+
throw new Error('MIAODA_SERVER_RUNTIME_FILE_SET_MISMATCH');
|
|
1055
|
+
}
|
|
1056
|
+
const dependencyFilePaths = serverDependencies.files.map(
|
|
1057
|
+
file => `server/node_modules/${file.path}`
|
|
1058
|
+
);
|
|
1059
|
+
if (
|
|
1060
|
+
JSON.stringify(dependencyFilePaths.sort()) !==
|
|
1061
|
+
JSON.stringify(
|
|
1062
|
+
sealedServerPaths
|
|
1063
|
+
.filter(file => file.startsWith('server/node_modules/'))
|
|
1064
|
+
.sort()
|
|
1065
|
+
)
|
|
1066
|
+
) {
|
|
1067
|
+
throw new Error('MIAODA_SERVER_RUNTIME_DEPENDENCY_SET_MISMATCH');
|
|
1068
|
+
}
|
|
1069
|
+
const bundleFile = path.join(
|
|
1070
|
+
resolvedCacheRoot,
|
|
1071
|
+
'server',
|
|
1072
|
+
'bundle',
|
|
1073
|
+
'server.bundle.cjs'
|
|
1074
|
+
);
|
|
1075
|
+
const bundleMetadataFile = `${bundleFile}.meta.json`;
|
|
1076
|
+
const bundleExists = fs.existsSync(bundleFile);
|
|
1077
|
+
const bundleMetadataExists = fs.existsSync(bundleMetadataFile);
|
|
1078
|
+
if (bundleExists !== bundleMetadataExists) {
|
|
1079
|
+
throw new Error('MIAODA_SERVER_TRANSITION_BUNDLE_INCOMPLETE');
|
|
1080
|
+
}
|
|
1081
|
+
if (bundleExists) {
|
|
1082
|
+
const bundleMetadata = readJson(bundleMetadataFile);
|
|
1083
|
+
if (
|
|
1084
|
+
bundleMetadata?.schemaVersion !== 2 ||
|
|
1085
|
+
bundleMetadata?.consumable !== true ||
|
|
1086
|
+
!/^[a-f0-9]{64}$/.test(bundleMetadata?.workspaceSourceSha256 || '') ||
|
|
1087
|
+
sha256(bundleFile) !== bundleMetadata.bundleSha256
|
|
1088
|
+
) {
|
|
1089
|
+
throw new Error('MIAODA_SERVER_TRANSITION_BUNDLE_REJECTED');
|
|
1090
|
+
}
|
|
1091
|
+
// Schema v4 seals the optional transition bundle through `serverFiles`
|
|
1092
|
+
// instead of the legacy `artifacts.serverBundle` field. Expose the
|
|
1093
|
+
// validated paths under the stable logical names used by the runtime.
|
|
1094
|
+
// Validation receipts will consequently protect these files as controls.
|
|
1095
|
+
artifacts.serverBundle = bundleFile;
|
|
1096
|
+
artifacts.serverMetadata = bundleMetadataFile;
|
|
1097
|
+
serverMetadata = bundleMetadata;
|
|
1098
|
+
}
|
|
1099
|
+
const cachedActionPlugins = Object.fromEntries(
|
|
1100
|
+
serverDependencies.actionPlugins.map(plugin => [
|
|
1101
|
+
plugin.name,
|
|
1102
|
+
plugin.version,
|
|
1103
|
+
])
|
|
1104
|
+
);
|
|
1105
|
+
if (
|
|
1106
|
+
JSON.stringify(
|
|
1107
|
+
workspaceBuildInputs(resolvedProjectRoot).actionPlugins
|
|
1108
|
+
) !== JSON.stringify(canonicalJson(cachedActionPlugins))
|
|
1109
|
+
) {
|
|
1110
|
+
throw new Error('MIAODA_SERVER_RUNTIME_ACTION_PLUGIN_MISMATCH');
|
|
1111
|
+
}
|
|
1112
|
+
if (sealedServerFiles.length !== generation.serverFiles.length) {
|
|
1113
|
+
throw new Error('MIAODA_SERVER_RUNTIME_FILE_SET_MISMATCH');
|
|
1114
|
+
}
|
|
1115
|
+
const expectedRuntime = generation.serverRuntime || {};
|
|
1116
|
+
if (
|
|
1117
|
+
String(expectedRuntime.nodeModuleAbi) !== process.versions.modules ||
|
|
1118
|
+
expectedRuntime.platform !== process.platform ||
|
|
1119
|
+
expectedRuntime.arch !== process.arch ||
|
|
1120
|
+
String(expectedRuntime.nodeVersion || '').replace(/^v/, '') !==
|
|
1121
|
+
process.versions.node
|
|
1122
|
+
) {
|
|
1123
|
+
throw new Error('MIAODA_SERVER_CACHE_ABI_MISMATCH');
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (validateServerArtifacts && generation.schemaVersion === 3) {
|
|
1127
|
+
serverMetadata = readJson(artifacts.serverMetadata);
|
|
1128
|
+
if (
|
|
1129
|
+
JSON.stringify(
|
|
1130
|
+
jsonIdentity(stableServerMetadataIdentity(serverMetadata))
|
|
1131
|
+
) !== JSON.stringify(generation.serverMetadataIdentity)
|
|
1132
|
+
) {
|
|
1133
|
+
throw new Error('MIAODA_CACHE_SERVER_METADATA_IDENTITY_MISMATCH');
|
|
1134
|
+
}
|
|
1135
|
+
const expectedRuntime = generation.serverRuntime || {};
|
|
1136
|
+
const expectedNodeVersion = String(
|
|
1137
|
+
expectedRuntime.nodeVersion || ''
|
|
1138
|
+
).replace(/^v/, '');
|
|
1139
|
+
const requiredProbeChecks = [
|
|
1140
|
+
'isolated-node-modules',
|
|
1141
|
+
'runtime-assets',
|
|
1142
|
+
'http-readiness',
|
|
1143
|
+
'business-api',
|
|
1144
|
+
'process-alive',
|
|
1145
|
+
'module-resolution',
|
|
1146
|
+
];
|
|
1147
|
+
const probeChecks = serverMetadata.availabilityProbe?.checks;
|
|
1148
|
+
const probeCheckNames = Array.isArray(probeChecks)
|
|
1149
|
+
? probeChecks.map(check => check?.name)
|
|
1150
|
+
: [];
|
|
1151
|
+
if (
|
|
1152
|
+
serverMetadata.schemaVersion !== 2 ||
|
|
1153
|
+
serverMetadata.consumable !== true ||
|
|
1154
|
+
!Array.isArray(serverMetadata.runtimeAssets) ||
|
|
1155
|
+
!Array.isArray(serverMetadata.externalImports) ||
|
|
1156
|
+
serverMetadata.externalImports.length > 0 ||
|
|
1157
|
+
!Array.isArray(serverMetadata.runtimeAssetWarnings) ||
|
|
1158
|
+
serverMetadata.runtimeAssetWarnings.some(
|
|
1159
|
+
warning =>
|
|
1160
|
+
typeof warning?.kind !== 'string' ||
|
|
1161
|
+
typeof warning?.source !== 'string' ||
|
|
1162
|
+
!/^[a-f0-9]{16}$/.test(warning?.expressionHash || '') ||
|
|
1163
|
+
typeof warning?.message !== 'string'
|
|
1164
|
+
) ||
|
|
1165
|
+
!Array.isArray(serverMetadata.actionPlugins) ||
|
|
1166
|
+
serverMetadata.actionPlugins.some(
|
|
1167
|
+
plugin =>
|
|
1168
|
+
typeof plugin?.name !== 'string' ||
|
|
1169
|
+
!plugin.name ||
|
|
1170
|
+
typeof plugin?.version !== 'string' ||
|
|
1171
|
+
!plugin.version
|
|
1172
|
+
) ||
|
|
1173
|
+
!Array.isArray(serverMetadata.dynamicImportWarnings) ||
|
|
1174
|
+
serverMetadata.dynamicImportWarnings.some(
|
|
1175
|
+
warning =>
|
|
1176
|
+
typeof warning?.kind !== 'string' ||
|
|
1177
|
+
typeof warning?.source !== 'string' ||
|
|
1178
|
+
!/^[a-f0-9]{16}$/.test(warning?.expressionHash || '') ||
|
|
1179
|
+
typeof warning?.message !== 'string'
|
|
1180
|
+
) ||
|
|
1181
|
+
serverMetadata.bundleSha256 !==
|
|
1182
|
+
generation.artifacts.serverBundle.sha256 ||
|
|
1183
|
+
serverMetadata.availabilityProbe?.success !== true ||
|
|
1184
|
+
serverMetadata.availabilityProbe?.nodeModulesPresent !== false ||
|
|
1185
|
+
!Array.isArray(probeChecks) ||
|
|
1186
|
+
probeChecks.some(
|
|
1187
|
+
check => typeof check?.name !== 'string' || check?.success !== true
|
|
1188
|
+
) ||
|
|
1189
|
+
new Set(probeCheckNames).size !== probeCheckNames.length ||
|
|
1190
|
+
requiredProbeChecks.some(name => !probeCheckNames.includes(name)) ||
|
|
1191
|
+
(serverMetadata.actionPlugins.length > 0 &&
|
|
1192
|
+
!probeCheckNames.includes('action-plugin-registry'))
|
|
1193
|
+
) {
|
|
1194
|
+
throw new Error('MIAODA_SERVER_CACHE_METADATA_REJECTED');
|
|
1195
|
+
}
|
|
1196
|
+
const expectedRuntimeAssetKeys = [];
|
|
1197
|
+
const runtimeAssetPaths = new Set();
|
|
1198
|
+
for (const asset of serverMetadata.runtimeAssets) {
|
|
1199
|
+
const assetPath = asset?.path;
|
|
1200
|
+
if (
|
|
1201
|
+
typeof assetPath !== 'string' ||
|
|
1202
|
+
!assetPath ||
|
|
1203
|
+
assetPath !== path.posix.normalize(assetPath) ||
|
|
1204
|
+
assetPath === '.' ||
|
|
1205
|
+
assetPath.startsWith('../') ||
|
|
1206
|
+
path.posix.isAbsolute(assetPath) ||
|
|
1207
|
+
assetPath.includes('\\') ||
|
|
1208
|
+
runtimeAssetPaths.has(assetPath)
|
|
1209
|
+
) {
|
|
1210
|
+
throw new Error('MIAODA_SERVER_CACHE_RUNTIME_ASSET_REJECTED');
|
|
1211
|
+
}
|
|
1212
|
+
runtimeAssetPaths.add(assetPath);
|
|
1213
|
+
const key = `serverRuntimeAsset:${assetPath}`;
|
|
1214
|
+
const generationArtifact = generation.artifacts[key];
|
|
1215
|
+
if (
|
|
1216
|
+
!generationArtifact ||
|
|
1217
|
+
generationArtifact.path !== path.posix.join('server', assetPath) ||
|
|
1218
|
+
generationArtifact.sha256 !== asset.sha256 ||
|
|
1219
|
+
generationArtifact.size !== asset.size ||
|
|
1220
|
+
!artifacts[key]
|
|
1221
|
+
) {
|
|
1222
|
+
throw new Error('MIAODA_SERVER_CACHE_RUNTIME_ASSET_REJECTED');
|
|
1223
|
+
}
|
|
1224
|
+
expectedRuntimeAssetKeys.push(key);
|
|
1225
|
+
}
|
|
1226
|
+
const sealedRuntimeAssetKeys = Object.keys(generation.artifacts)
|
|
1227
|
+
.filter(key => key.startsWith('serverRuntimeAsset:'))
|
|
1228
|
+
.sort();
|
|
1229
|
+
if (
|
|
1230
|
+
JSON.stringify(expectedRuntimeAssetKeys.sort()) !==
|
|
1231
|
+
JSON.stringify(sealedRuntimeAssetKeys)
|
|
1232
|
+
) {
|
|
1233
|
+
throw new Error('MIAODA_SERVER_CACHE_RUNTIME_ASSET_SET_MISMATCH');
|
|
1234
|
+
}
|
|
1235
|
+
const sealedServerFiles = Object.values(generation.artifacts)
|
|
1236
|
+
.map(artifact => artifact?.path)
|
|
1237
|
+
.filter(
|
|
1238
|
+
artifactPath =>
|
|
1239
|
+
typeof artifactPath === 'string' && artifactPath.startsWith('server/')
|
|
1240
|
+
)
|
|
1241
|
+
.sort();
|
|
1242
|
+
const actualServerFiles = listRegularFiles(
|
|
1243
|
+
path.join(resolvedCacheRoot, 'server'),
|
|
1244
|
+
resolvedCacheRoot
|
|
1245
|
+
);
|
|
1246
|
+
if (
|
|
1247
|
+
JSON.stringify(actualServerFiles) !== JSON.stringify(sealedServerFiles)
|
|
1248
|
+
) {
|
|
1249
|
+
throw new Error('MIAODA_SERVER_CACHE_FILE_SET_MISMATCH');
|
|
1250
|
+
}
|
|
1251
|
+
if (
|
|
1252
|
+
expectedRuntime.nodeModuleAbi !== process.versions.modules ||
|
|
1253
|
+
expectedRuntime.platform !== process.platform ||
|
|
1254
|
+
expectedRuntime.arch !== process.arch ||
|
|
1255
|
+
expectedNodeVersion !== process.versions.node
|
|
1256
|
+
) {
|
|
1257
|
+
throw new Error('MIAODA_SERVER_CACHE_ABI_MISMATCH');
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (
|
|
1261
|
+
(validateViteArtifacts && appRuntime.schemaVersion !== 1) ||
|
|
1262
|
+
(validateViteArtifacts &&
|
|
1263
|
+
appRuntime.runtimeAbiHash !== generation.runtimeAbiHash)
|
|
1264
|
+
) {
|
|
1265
|
+
throw new Error('MIAODA_APP_RUNTIME_ABI_MISMATCH');
|
|
1266
|
+
}
|
|
1267
|
+
if (validateViteArtifacts) {
|
|
1268
|
+
validateClientConfigFiles(resolvedProjectRoot, appRuntime);
|
|
1269
|
+
}
|
|
1270
|
+
if (
|
|
1271
|
+
(validateViteArtifacts &&
|
|
1272
|
+
!/^[a-f0-9]{64}$/.test(appRuntime.clientDependencyGraphHash || '')) ||
|
|
1273
|
+
(validateViteArtifacts &&
|
|
1274
|
+
(appRuntime.clientDependencyGraph?.schemaVersion !== 1 ||
|
|
1275
|
+
appRuntime.clientDependencyGraph?.runtimeAbiHash !==
|
|
1276
|
+
generation.runtimeAbiHash))
|
|
1277
|
+
) {
|
|
1278
|
+
throw new Error('MIAODA_CLIENT_DEPENDENCY_GRAPH_INVALID');
|
|
1279
|
+
}
|
|
1280
|
+
if (validateViteArtifacts && validateConfigEnvironment) {
|
|
1281
|
+
validateClientConfigEnvironment(appRuntime);
|
|
1282
|
+
}
|
|
1283
|
+
if (validateViteArtifacts) {
|
|
1284
|
+
const optimizedDependencies = new Set(
|
|
1285
|
+
Object.keys(viteMetadata.optimized || {})
|
|
1286
|
+
);
|
|
1287
|
+
const missingViteDependencies = Array.from(
|
|
1288
|
+
new Set(appRuntime.optimizeDependencies || [])
|
|
1289
|
+
).filter(specifier => !optimizedDependencies.has(specifier));
|
|
1290
|
+
if (missingViteDependencies.length > 0) {
|
|
1291
|
+
throw new Error(
|
|
1292
|
+
`MIAODA_VITE_CACHE_DEPENDENCY_COVERAGE_MISSING: ${missingViteDependencies.join(', ')}`
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
const graphPackageNames = new Set(
|
|
1296
|
+
(appRuntime.clientDependencyGraph?.entries || [])
|
|
1297
|
+
.map(entry => packageNameFromOptimizerId(entry.specifier || ''))
|
|
1298
|
+
.filter(Boolean)
|
|
1299
|
+
);
|
|
1300
|
+
const optimizerEntries = [
|
|
1301
|
+
...Object.entries(viteMetadata.optimized || {}),
|
|
1302
|
+
...Object.entries(viteMetadata.discovered || {}),
|
|
1303
|
+
];
|
|
1304
|
+
const uncoveredOptimizerPackages = Array.from(
|
|
1305
|
+
new Set(
|
|
1306
|
+
optimizerEntries.flatMap(([id, entry]) => {
|
|
1307
|
+
const name = packageNameFromOptimizerId(id);
|
|
1308
|
+
if (!name || graphPackageNames.has(name)) return [];
|
|
1309
|
+
return optimizerEntryUsesRuntimePackage({
|
|
1310
|
+
projectRoot: resolvedProjectRoot,
|
|
1311
|
+
runtimeManifestFile: resolvedRuntimeManifest,
|
|
1312
|
+
runtime,
|
|
1313
|
+
packageName: name,
|
|
1314
|
+
entry,
|
|
1315
|
+
})
|
|
1316
|
+
? []
|
|
1317
|
+
: [name];
|
|
1318
|
+
})
|
|
1319
|
+
)
|
|
1320
|
+
).sort();
|
|
1321
|
+
if (uncoveredOptimizerPackages.length > 0) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
`MIAODA_VITE_OPTIMIZER_GRAPH_TOOLCHAIN_COVERAGE_MISSING: ${uncoveredOptimizerPackages.join(', ')}`
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
projectRoot: resolvedProjectRoot,
|
|
1329
|
+
cacheRoot: resolvedCacheRoot,
|
|
1330
|
+
runtimeManifestFile: resolvedRuntimeManifest,
|
|
1331
|
+
generationFile,
|
|
1332
|
+
generation,
|
|
1333
|
+
runtime,
|
|
1334
|
+
artifacts,
|
|
1335
|
+
serverMetadata,
|
|
1336
|
+
serverDependencies,
|
|
1337
|
+
appRuntime,
|
|
1338
|
+
viteFiles,
|
|
1339
|
+
validation: {
|
|
1340
|
+
artifactScope,
|
|
1341
|
+
validateConfigEnvironment,
|
|
1342
|
+
validateWorkspaceInputs,
|
|
1343
|
+
validatedBy: 'full-sha',
|
|
1344
|
+
},
|
|
1345
|
+
};
|
|
1346
|
+
}
|