@somewhere-tech/cli 0.31.16 → 0.31.18
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/commands/browser.js +1 -1
- package/dist/commands/browser.js.map +1 -1
- package/dist/commands/check.js +1 -2
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/db.js +8 -5
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/dev.js +18 -383
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/status.js +0 -36
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/typecheck.js +1 -1
- package/dist/commands/typecheck.js.map +1 -1
- package/dist/commands/verify.js +1 -1
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/{local → lib}/browser-run.js +1 -1
- package/dist/lib/browser-run.js.map +1 -0
- package/dist/lib/chrome.js.map +1 -0
- package/dist/lib/envfile-write.js +2 -2
- package/dist/lib/envfile-write.js.map +1 -1
- package/dist/lib/frontend-dev.js +122 -0
- package/dist/lib/frontend-dev.js.map +1 -0
- package/dist/lib/init-agent-guide.js +15 -19
- package/dist/lib/init-agent-guide.js.map +1 -1
- package/dist/lib/init-green-template.js +1 -1
- package/dist/lib/project-urls.js +13 -0
- package/dist/lib/project-urls.js.map +1 -1
- package/npm-shrinkwrap.json +2 -17
- package/package.json +3 -5
- package/runtime/VENDOR.json +0 -2
- package/dist/commands/exec.js +0 -145
- package/dist/commands/exec.js.map +0 -1
- package/dist/local/browser-run.js.map +0 -1
- package/dist/local/chrome.js.map +0 -1
- package/dist/local/compiler-core.js +0 -51
- package/dist/local/compiler-core.js.map +0 -1
- package/dist/local/compiler.js +0 -683
- package/dist/local/compiler.js.map +0 -1
- package/dist/local/dev-server.js +0 -517
- package/dist/local/dev-server.js.map +0 -1
- package/dist/local/envfile.js +0 -40
- package/dist/local/envfile.js.map +0 -1
- package/dist/local/loader.js +0 -299
- package/dist/local/loader.js.map +0 -1
- package/dist/local/loopback.js +0 -129
- package/dist/local/loopback.js.map +0 -1
- package/dist/local/request-timing.js +0 -101
- package/dist/local/request-timing.js.map +0 -1
- package/dist/local/router.js +0 -114
- package/dist/local/router.js.map +0 -1
- package/dist/local/runtime.js +0 -345
- package/dist/local/runtime.js.map +0 -1
- package/dist/local/server.js +0 -309
- package/dist/local/server.js.map +0 -1
- package/dist/local/telemetry-filter.js +0 -120
- package/dist/local/telemetry-filter.js.map +0 -1
- package/node_modules/esbuild-wasm/LICENSE.md +0 -21
- package/node_modules/esbuild-wasm/README.md +0 -3
- package/node_modules/esbuild-wasm/bin/esbuild +0 -91
- package/node_modules/esbuild-wasm/esbuild.wasm +0 -0
- package/node_modules/esbuild-wasm/esm/browser.d.ts +0 -705
- package/node_modules/esbuild-wasm/esm/browser.js +0 -2393
- package/node_modules/esbuild-wasm/esm/browser.min.js +0 -20
- package/node_modules/esbuild-wasm/lib/browser.d.ts +0 -705
- package/node_modules/esbuild-wasm/lib/browser.js +0 -2438
- package/node_modules/esbuild-wasm/lib/browser.min.js +0 -22
- package/node_modules/esbuild-wasm/lib/main.d.ts +0 -705
- package/node_modules/esbuild-wasm/lib/main.js +0 -2051
- package/node_modules/esbuild-wasm/package.json +0 -19
- package/node_modules/esbuild-wasm/wasm_exec.js +0 -561
- package/node_modules/esbuild-wasm/wasm_exec_node.js +0 -39
- package/runtime/compiler/VENDOR.json +0 -64
- package/runtime/compiler/compile-core.cjs +0 -2007
- package/runtime/compiler/graph-contract.cjs +0 -124
- package/runtime/compiler/typed-functions.cjs +0 -575
- package/runtime/platform-context.mjs +0 -8193
- package/runtime/sw-init.mjs +0 -283
- /package/dist/{local → lib}/chrome.js +0 -0
|
@@ -1,2007 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* compile-core — the somewhere.tech compiler, as a host-parameterized module.
|
|
3
|
-
*
|
|
4
|
-
* This file IS the compiler: source validation, the esbuild project/function/
|
|
5
|
-
* transform builds, the svg + url-asset plugins, Tailwind v3/v4 via real
|
|
6
|
-
* PostCSS, import.meta.env VITE_* / process.env.REACT_APP_* defines, tsconfig
|
|
7
|
-
* path aliases, dependency resolution semantics, phantom-import detection,
|
|
8
|
-
* dependency review, and the artifact manifest. It was extracted verbatim from
|
|
9
|
-
* containers/compile/server.js so that BOTH callers run one implementation:
|
|
10
|
-
*
|
|
11
|
-
* - the compile container (server.js) — deploy, over HTTP;
|
|
12
|
-
* - the CLI's `somewhere dev` local loop — which vendors this file.
|
|
13
|
-
*
|
|
14
|
-
* server.js keeps only what is genuinely the container's: the HTTP server, the
|
|
15
|
-
* build queue, the BUILD_TIMEOUT controller, direct-to-storage artifact upload,
|
|
16
|
-
* and the baked-image identity (which node_modules trees exist, how an install
|
|
17
|
-
* runs, what the compiler stamp is).
|
|
18
|
-
*
|
|
19
|
-
* ── The boundary ──────────────────────────────────────────────────────────
|
|
20
|
-
* Everything at module scope below is PURE: it depends only on its arguments,
|
|
21
|
-
* node builtins, and esbuild. Everything inside createCompileCore(host) is
|
|
22
|
-
* ENVIRONMENT-BOUND: it needs to know where the baked dependency trees live,
|
|
23
|
-
* how to require the toolchain (typescript / postcss / autoprefixer /
|
|
24
|
-
* tailwindcss / semver), how to install a missing dependency, and what stamp
|
|
25
|
-
* identifies this compiler build. That is the entire difference between the
|
|
26
|
-
* container and the CLI, and `host` is the entire list of it.
|
|
27
|
-
*
|
|
28
|
-
* host = {
|
|
29
|
-
* imageNodeModules, // string | string[] — the dependency search path, in
|
|
30
|
-
* // order, behind the build root; esbuild nodePaths
|
|
31
|
-
* react19NodeModules, // string|null — isolated React 19 set, or null
|
|
32
|
-
* tw4TailwindDir, // string|null — dir to link as node_modules/tailwindcss for v4
|
|
33
|
-
* esbuild, // optional — the esbuild module to compile with.
|
|
34
|
-
* // Defaults to require('esbuild'). The CLI passes
|
|
35
|
-
* // esbuild-wasm: same version, byte-identical
|
|
36
|
-
* // output, and ONE package instead of 24
|
|
37
|
-
* // platform-native ones, which is what lets the
|
|
38
|
-
* // published CLI keep every production dependency
|
|
39
|
-
* // inside its signed artifact.
|
|
40
|
-
* requireImage(spec), // (spec) => module, resolved from the toolchain tree
|
|
41
|
-
* requireTw4(spec), // (spec) => module, resolved from the v4 engine tree
|
|
42
|
-
* installPackages({ root, specs, ctx }), // => Promise<void>; throws on failure
|
|
43
|
-
* requiresPackageProxy, // default TRUE. The compile container is egress-
|
|
44
|
-
* // locked and can only reach the registry through a
|
|
45
|
-
* // per-build scoped proxy, so with no proxy in the
|
|
46
|
-
* // request it must refuse rather than build with
|
|
47
|
-
* // unresolved pins. A host that reaches npm
|
|
48
|
-
* // directly (the CLI, on the developer's own
|
|
49
|
-
* // machine, through their own npm config) sets
|
|
50
|
-
* // false. Defaulting to true keeps that refusal
|
|
51
|
-
* // something a host must deliberately opt out of.
|
|
52
|
-
* uploadArtifacts, // optional (artifactUpload, chunks) => Promise<manifest[]>
|
|
53
|
-
* stamp: { source, toolchain }, // compiler identity for this build
|
|
54
|
-
* }
|
|
55
|
-
*
|
|
56
|
-
* createCompileCore(host) => { compile, compileProject, compileTransforms,
|
|
57
|
-
* parseSources, ... } — the same function signatures server.js exported
|
|
58
|
-
* before the extraction, so its consumers and contract tests are unchanged.
|
|
59
|
-
*/
|
|
60
|
-
'use strict';
|
|
61
|
-
const fs = require('fs');
|
|
62
|
-
const fsp = require('fs/promises');
|
|
63
|
-
const os = require('os');
|
|
64
|
-
const path = require('path');
|
|
65
|
-
const crypto = require('crypto');
|
|
66
|
-
const graphContract = require('./graph-contract.cjs');
|
|
67
|
-
const typedFunctions = require('./typed-functions.cjs');
|
|
68
|
-
const {
|
|
69
|
-
GRAPH_MAX_EDGES,
|
|
70
|
-
GRAPH_MAX_BYTES,
|
|
71
|
-
importGraphFromMetafile,
|
|
72
|
-
graphFromMetafile,
|
|
73
|
-
} = graphContract;
|
|
74
|
-
|
|
75
|
-
const COMPILER_CONTRACT = 'somewhere-compiler-v2';
|
|
76
|
-
|
|
77
|
-
/* ══════════════════════════════════════════════════════════════════════════
|
|
78
|
-
* PURE — no knowledge of where anything is installed.
|
|
79
|
-
* ══════════════════════════════════════════════════════════════════════════ */
|
|
80
|
-
|
|
81
|
-
const MAX_TOTAL_SOURCE_BYTES = 5 * 1024 * 1024;
|
|
82
|
-
const MAX_FILE_COUNT = 1500;
|
|
83
|
-
const MAX_BINARY_BYTES = 25 * 1024 * 1024;
|
|
84
|
-
const MAX_ARTIFACT_COUNT = 3000;
|
|
85
|
-
|
|
86
|
-
// Node-builtin → browser-polyfill aliases (pre-baked in the image). esbuild for
|
|
87
|
-
// the browser doesn't polyfill Node builtins, so a dep that `import`s `buffer`
|
|
88
|
-
// (pdf-lib, docx, xlsx, …) otherwise hard-errors "Could not resolve buffer".
|
|
89
|
-
// Both bare + `node:`-prefixed forms are mapped here.
|
|
90
|
-
const NODE_POLYFILLS = {
|
|
91
|
-
buffer: 'buffer', 'node:buffer': 'buffer',
|
|
92
|
-
process: 'process/browser', 'node:process': 'process/browser',
|
|
93
|
-
stream: 'stream-browserify', 'node:stream': 'stream-browserify',
|
|
94
|
-
util: 'util', 'node:util': 'util',
|
|
95
|
-
events: 'events', 'node:events': 'events',
|
|
96
|
-
path: 'path-browserify', 'node:path': 'path-browserify',
|
|
97
|
-
crypto: 'crypto-browserify', 'node:crypto': 'crypto-browserify',
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// .svg is handled by svgPlugin (dual binding + ?react component), NOT a flat
|
|
101
|
-
// data-URL loader — so `import { ReactComponent }` / `?react` work like svgr.
|
|
102
|
-
const ASSET_LOADERS = {
|
|
103
|
-
'.png': 'dataurl', '.jpg': 'dataurl', '.jpeg': 'dataurl', '.gif': 'dataurl',
|
|
104
|
-
'.webp': 'dataurl', '.avif': 'dataurl', '.ico': 'dataurl', '.bmp': 'dataurl',
|
|
105
|
-
'.woff': 'dataurl', '.woff2': 'dataurl', '.ttf': 'dataurl',
|
|
106
|
-
'.otf': 'dataurl', '.eot': 'dataurl', '.mp3': 'dataurl', '.mp4': 'dataurl',
|
|
107
|
-
'.webm': 'dataurl', '.wav': 'dataurl', '.ogg': 'dataurl', '.pdf': 'dataurl',
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
function stableMapEntries(value) {
|
|
111
|
-
return Object.entries(value || {}).sort(([a], [b]) => a.localeCompare(b));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function sourceDigest(body) {
|
|
115
|
-
const canonical = {
|
|
116
|
-
entry: typeof body.entry === 'string' ? body.entry : null,
|
|
117
|
-
function_entries: [...(body.function_entries || [])].sort(),
|
|
118
|
-
function_paths: [...(body.function_paths || [])].sort(),
|
|
119
|
-
transform_entries: [...(body.transform_entries || [])].sort(),
|
|
120
|
-
files: stableMapEntries(body.files),
|
|
121
|
-
package_json: typeof body.package_json === 'string' ? body.package_json : null,
|
|
122
|
-
tsconfig: typeof body.tsconfig === 'string' ? body.tsconfig : null,
|
|
123
|
-
binary_files: stableMapEntries(body.binary_files),
|
|
124
|
-
binary_paths: [...(body.binary_paths || [])].sort(),
|
|
125
|
-
vite_env: stableMapEntries(body.vite_env),
|
|
126
|
-
};
|
|
127
|
-
return crypto.createHash('sha256').update(JSON.stringify(canonical)).digest('hex');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/* ─── SVG → React component (Vite/svgr-compatible) ────────────────────────
|
|
131
|
-
* `import Logo from './logo.svg?react'` → a component (default export).
|
|
132
|
-
* `import url, { ReactComponent } from './logo.svg'` → URL default + component.
|
|
133
|
-
* dangerouslySetInnerHTML so the SVG body never goes through the JSX parser
|
|
134
|
-
* (real SVGs carry <style>{…}</style>, xmlns:xlink, etc. that break naive JSX). */
|
|
135
|
-
const SVG_ATTR_RENAMES = {
|
|
136
|
-
'class': 'className', 'for': 'htmlFor', 'tabindex': 'tabIndex',
|
|
137
|
-
'stroke-width': 'strokeWidth', 'stroke-linecap': 'strokeLinecap', 'stroke-linejoin': 'strokeLinejoin',
|
|
138
|
-
'stroke-dasharray': 'strokeDasharray', 'stroke-dashoffset': 'strokeDashoffset', 'stroke-miterlimit': 'strokeMiterlimit',
|
|
139
|
-
'stroke-opacity': 'strokeOpacity', 'fill-opacity': 'fillOpacity', 'fill-rule': 'fillRule',
|
|
140
|
-
'clip-path': 'clipPath', 'clip-rule': 'clipRule', 'vector-effect': 'vectorEffect',
|
|
141
|
-
'shape-rendering': 'shapeRendering', 'text-anchor': 'textAnchor', 'font-family': 'fontFamily',
|
|
142
|
-
'font-size': 'fontSize', 'font-weight': 'fontWeight', 'stop-color': 'stopColor', 'stop-opacity': 'stopOpacity',
|
|
143
|
-
};
|
|
144
|
-
function svgToReactComponent(svgText, componentName, exportDefault) {
|
|
145
|
-
const cleaned = svgText
|
|
146
|
-
.replace(/<\?xml[^?]*\?>/g, '').replace(/<!DOCTYPE[^>]*>/g, '').replace(/<!--[\s\S]*?-->/g, '').trim();
|
|
147
|
-
const rootMatch = cleaned.match(/^<svg\b([^>]*)>([\s\S]*)<\/svg>\s*$/i);
|
|
148
|
-
const reactAttrs = {};
|
|
149
|
-
let inner = cleaned;
|
|
150
|
-
if (rootMatch) {
|
|
151
|
-
inner = rootMatch[2];
|
|
152
|
-
const attrRe = /([A-Za-z][\w:-]*)\s*=\s*"([^"]*)"/g;
|
|
153
|
-
let m;
|
|
154
|
-
while ((m = attrRe.exec(rootMatch[1]))) {
|
|
155
|
-
const key = SVG_ATTR_RENAMES[m[1]] || (m[1].includes(':') ? null : m[1]);
|
|
156
|
-
if (key) reactAttrs[key] = m[2];
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
const decl = `import * as React from 'react';
|
|
160
|
-
function ${componentName}(props) {
|
|
161
|
-
return React.createElement('svg', Object.assign(
|
|
162
|
-
${JSON.stringify(reactAttrs)}, props,
|
|
163
|
-
{ dangerouslySetInnerHTML: { __html: ${JSON.stringify(inner)} } },
|
|
164
|
-
));
|
|
165
|
-
}
|
|
166
|
-
`;
|
|
167
|
-
return exportDefault === false ? decl : decl + `export default ${componentName};\n`;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function svgDataUrl(svgText) {
|
|
171
|
-
return `data:image/svg+xml;base64,${Buffer.from(svgText, 'utf8').toString('base64')}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* esbuild plugin for the two svg import shapes.
|
|
176
|
-
*
|
|
177
|
-
* `root` is the build root, and the `?react` branch keys its namespace on the
|
|
178
|
-
* ROOT-RELATIVE path rather than the absolute one. esbuild stamps
|
|
179
|
-
* `namespace:path` into the bundle banner and the sourcemap, and every build
|
|
180
|
-
* materializes to a fresh mkdtemp dir — so the absolute form put a random tmp
|
|
181
|
-
* segment in the OUTPUT BYTES, and identical source compiled to a different
|
|
182
|
-
* content hash every single time (tsk_cdd7c47c). Files loaded through the
|
|
183
|
-
* normal pipeline are already normalized against absWorkingDir; a custom
|
|
184
|
-
* namespace is not, so it has to normalize itself.
|
|
185
|
-
*/
|
|
186
|
-
function svgPlugin(root) {
|
|
187
|
-
let realRoot = root;
|
|
188
|
-
try { realRoot = fs.realpathSync(root); } catch { /* keep root */ }
|
|
189
|
-
// esbuild reports resolveDir as a REAL path (symlinks resolved); root may
|
|
190
|
-
// contain one (macOS /var → /private/var), so compare like with like.
|
|
191
|
-
const toAbs = (rel) => path.resolve(realRoot, rel);
|
|
192
|
-
return {
|
|
193
|
-
name: 'somewhere-svg',
|
|
194
|
-
setup(build) {
|
|
195
|
-
// `./x.svg?react` — strip the query, resolve on disk, emit a component.
|
|
196
|
-
build.onResolve({ filter: /\.svg\?react$/ }, (args) => {
|
|
197
|
-
const clean = args.path.replace(/\?react$/, '');
|
|
198
|
-
const abs = path.isAbsolute(clean) ? clean : path.resolve(args.resolveDir, clean);
|
|
199
|
-
const rel = path.relative(realRoot, abs);
|
|
200
|
-
// Outside the build root (nothing normal reaches here) — keep the
|
|
201
|
-
// absolute path rather than emit a path that escapes the root.
|
|
202
|
-
if (rel.startsWith('..') || path.isAbsolute(rel)) return { path: abs, namespace: 'svg-react' };
|
|
203
|
-
return { path: rel, namespace: 'svg-react' };
|
|
204
|
-
});
|
|
205
|
-
build.onLoad({ filter: /.*/, namespace: 'svg-react' }, async (args) => {
|
|
206
|
-
const abs = path.isAbsolute(args.path) ? args.path : toAbs(args.path);
|
|
207
|
-
const svg = await fsp.readFile(abs, 'utf8');
|
|
208
|
-
const base = (abs.split('/').pop() || 'Icon').replace(/\.svg$/i, '').replace(/[^A-Za-z0-9]/g, '_').replace(/^([0-9])/, '_$1');
|
|
209
|
-
const name = (base.charAt(0).toUpperCase() + base.slice(1)) || 'SvgIcon';
|
|
210
|
-
return { contents: svgToReactComponent(svg, name, true), loader: 'jsx', resolveDir: path.dirname(abs) };
|
|
211
|
-
});
|
|
212
|
-
// bare `./x.svg` — URL default + named ReactComponent (svgr dual binding).
|
|
213
|
-
build.onLoad({ filter: /\.svg$/ }, async (args) => {
|
|
214
|
-
const svg = await fsp.readFile(args.path, 'utf8');
|
|
215
|
-
return {
|
|
216
|
-
contents:
|
|
217
|
-
svgToReactComponent(svg, '__SvgReactComponent', false) +
|
|
218
|
-
`\nexport const ReactComponent = __SvgReactComponent;` +
|
|
219
|
-
`\nexport default ${JSON.stringify(svgDataUrl(svg))};\n`,
|
|
220
|
-
loader: 'jsx',
|
|
221
|
-
resolveDir: path.dirname(args.path),
|
|
222
|
-
};
|
|
223
|
-
});
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/* ─── URL-export resolution for large binary assets (tsk_ad51a448) ───────────
|
|
229
|
-
* The worker withholds the BYTES of large binaries (the multi-MB base64 map
|
|
230
|
-
* was the memory spike that killed ~25MB deploy payloads) and sends a
|
|
231
|
-
* `binary_paths` manifest instead. Imports of those paths resolve to a URL
|
|
232
|
-
* export — `import img from './hero.png'` → "/hero.png" — which is what the
|
|
233
|
-
* asset's canonical serving path is anyway (Vite emits URLs, not data URLs,
|
|
234
|
-
* for anything past its inline threshold too). CSS url() tokens stay as
|
|
235
|
-
* external absolute URLs. Small assets keep arriving with bytes and ride the
|
|
236
|
-
* dataurl loaders, unchanged. */
|
|
237
|
-
function urlAssetPlugin(root, binaryPaths) {
|
|
238
|
-
const rels = new Set(binaryPaths || []);
|
|
239
|
-
// esbuild reports resolveDir as a REAL path (symlinks resolved); root may
|
|
240
|
-
// contain one (macOS /var → /private/var in local dev). Compare like with
|
|
241
|
-
// like or every relative() result walks out of the tree.
|
|
242
|
-
let realRoot = root;
|
|
243
|
-
try { realRoot = fs.realpathSync(root); } catch { /* keep root */ }
|
|
244
|
-
return {
|
|
245
|
-
name: 'somewhere-url-asset',
|
|
246
|
-
setup(build) {
|
|
247
|
-
if (rels.size === 0) return;
|
|
248
|
-
build.onResolve({ filter: /\.(png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|otf|eot|mp3|mp4|webm|wav|ogg|pdf)$/i }, (args) => {
|
|
249
|
-
if (args.kind === 'entry-point') return null;
|
|
250
|
-
const abs = path.isAbsolute(args.path) ? args.path : path.resolve(args.resolveDir, args.path);
|
|
251
|
-
const rel = path.relative(realRoot, abs);
|
|
252
|
-
if (!rels.has(rel)) return null; // bytes were provided → default pipeline
|
|
253
|
-
const url = `/${rel}`;
|
|
254
|
-
if (args.kind === 'url-token') return { path: url, external: true }; // css url(...)
|
|
255
|
-
return { path: url, namespace: 'sw-url-asset' };
|
|
256
|
-
});
|
|
257
|
-
build.onLoad({ filter: /.*/, namespace: 'sw-url-asset' }, (args) => ({
|
|
258
|
-
contents: `export default ${JSON.stringify(args.path)};`,
|
|
259
|
-
loader: 'js',
|
|
260
|
-
}));
|
|
261
|
-
},
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/* ─── function-vs-static classification (mirror of deploy.ts:isFunctionPath) ─ */
|
|
266
|
-
function isFunctionPath(p) {
|
|
267
|
-
if (!/\.(ts|js|mjs|tsx)$/i.test(p)) return false;
|
|
268
|
-
if (p.startsWith('api/') || p.startsWith('_lib/')) return true;
|
|
269
|
-
if (/^\[[^/]+\]\.(ts|js|mjs)$/.test(p)) return true;
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/* ─── Tailwind detection ────────────────────────────────────────────────── */
|
|
274
|
-
function detectTailwind(files) {
|
|
275
|
-
for (const [p, content] of Object.entries(files)) {
|
|
276
|
-
if (typeof content !== 'string') continue;
|
|
277
|
-
if (!/\.(?:css|scss|sass|less)$/i.test(p)) continue;
|
|
278
|
-
if (/@import\s+["']tailwindcss(?:\/[^"']*)?["']/.test(content)) return 4;
|
|
279
|
-
if (/@(?:theme|utility|custom-variant|source)\b/.test(content)) return 4;
|
|
280
|
-
}
|
|
281
|
-
for (const p of Object.keys(files)) {
|
|
282
|
-
if (/(?:^|\/)tailwind\.config\.(?:js|cjs|mjs|ts)$/.test(p)) return 3;
|
|
283
|
-
}
|
|
284
|
-
for (const [p, content] of Object.entries(files)) {
|
|
285
|
-
if (typeof content !== 'string') continue;
|
|
286
|
-
if (/(?:^|\/)postcss\.config\.(?:js|cjs|mjs|ts)$/.test(p) && /tailwind/i.test(content)) return 3;
|
|
287
|
-
if (!/\.(?:css|scss|sass|less)$/i.test(p)) continue;
|
|
288
|
-
if (/@tailwind\b/.test(content)) return 3;
|
|
289
|
-
if (/@apply\b/.test(content)) return 3;
|
|
290
|
-
if (/@layer\s+(?:base|components|utilities)\b/.test(content)) return 3;
|
|
291
|
-
}
|
|
292
|
-
return 0;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/* ─── import.meta.env / VITE_* defines ──────────────────────────────────── */
|
|
296
|
-
function buildViteEnvDefines(env, sourceFiles) {
|
|
297
|
-
const out = {};
|
|
298
|
-
for (const [k, v] of Object.entries(env || {})) {
|
|
299
|
-
if (!/^VITE_[A-Z0-9_]+$/.test(k)) continue;
|
|
300
|
-
out[`import.meta.env.${k}`] = JSON.stringify(v);
|
|
301
|
-
}
|
|
302
|
-
// referenced-but-unset VITE_* → "" so `VITE_X || 'fallback'` survives bundling
|
|
303
|
-
// instead of becoming `undefined.VITE_X` (the railtime blank-page class).
|
|
304
|
-
const referenced = new Set();
|
|
305
|
-
const rx = /import\.meta\.env\.(VITE_[A-Z0-9_]+)/g;
|
|
306
|
-
for (const src of Object.values(sourceFiles || {})) {
|
|
307
|
-
if (typeof src !== 'string') continue;
|
|
308
|
-
let m;
|
|
309
|
-
while ((m = rx.exec(src)) !== null) referenced.add(m[1]);
|
|
310
|
-
}
|
|
311
|
-
for (const name of referenced) {
|
|
312
|
-
const key = `import.meta.env.${name}`;
|
|
313
|
-
if (out[key] === undefined) out[key] = '""';
|
|
314
|
-
}
|
|
315
|
-
return out;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** CRA-style env: every referenced `process.env.REACT_APP_*` is defined from
|
|
319
|
-
* the project env map (or "" when unset) — react-scripts baked these into
|
|
320
|
-
* the build the same way. Without a define, the identifier survives into
|
|
321
|
-
* the bundle and `process` is undefined in the browser → blank page, so
|
|
322
|
-
* this can only FIX projects, never change a working one (an existing
|
|
323
|
-
* guarded `typeof process !== 'undefined'` check still short-circuits
|
|
324
|
-
* first). (tsk_4d9c865a) */
|
|
325
|
-
function buildReactEnvDefines(env, sourceFiles) {
|
|
326
|
-
const out = {};
|
|
327
|
-
const referenced = new Set();
|
|
328
|
-
const rx = /process\.env\.(REACT_APP_[A-Z0-9_]+)/g;
|
|
329
|
-
for (const src of Object.values(sourceFiles || {})) {
|
|
330
|
-
if (typeof src !== 'string') continue;
|
|
331
|
-
let m;
|
|
332
|
-
while ((m = rx.exec(src)) !== null) referenced.add(m[1]);
|
|
333
|
-
}
|
|
334
|
-
for (const name of referenced) {
|
|
335
|
-
const v = (env || {})[name];
|
|
336
|
-
out[`process.env.${name}`] = v === undefined ? '""' : JSON.stringify(v);
|
|
337
|
-
}
|
|
338
|
-
return out;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/** Write the in-memory file map (+ base64 binaries) to a fresh tmp dir. */
|
|
342
|
-
async function materialize(files, binaryFiles) {
|
|
343
|
-
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'swc-'));
|
|
344
|
-
for (const [rel, content] of Object.entries(files || {})) {
|
|
345
|
-
if (typeof content !== 'string') continue;
|
|
346
|
-
const abs = path.join(root, rel);
|
|
347
|
-
await fsp.mkdir(path.dirname(abs), { recursive: true });
|
|
348
|
-
await fsp.writeFile(abs, content, 'utf8');
|
|
349
|
-
}
|
|
350
|
-
for (const [rel, b64] of Object.entries(binaryFiles || {})) {
|
|
351
|
-
if (typeof b64 !== 'string') continue;
|
|
352
|
-
const abs = path.join(root, rel);
|
|
353
|
-
await fsp.mkdir(path.dirname(abs), { recursive: true });
|
|
354
|
-
await fsp.writeFile(abs, Buffer.from(b64, 'base64'));
|
|
355
|
-
}
|
|
356
|
-
return root;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function assertSourcePath(rel) {
|
|
360
|
-
if (typeof rel !== 'string' || !rel || rel.includes('\0') || rel.includes('\\') || path.isAbsolute(rel)) {
|
|
361
|
-
throw codedError('INVALID_SOURCE_PATH', `invalid source path: ${JSON.stringify(rel)}`);
|
|
362
|
-
}
|
|
363
|
-
const normalized = path.posix.normalize(rel);
|
|
364
|
-
if (normalized !== rel || normalized === '..' || normalized.startsWith('../')) {
|
|
365
|
-
throw codedError('INVALID_SOURCE_PATH', `source path is not canonical: ${rel}`);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function validateCompileInput(body) {
|
|
370
|
-
if (!body || typeof body !== 'object') throw new Error('request body is required');
|
|
371
|
-
if (!body.files || typeof body.files !== 'object' || Array.isArray(body.files)) {
|
|
372
|
-
throw new Error('`files` map is required');
|
|
373
|
-
}
|
|
374
|
-
if (body.binary_files !== undefined && (!body.binary_files || typeof body.binary_files !== 'object' || Array.isArray(body.binary_files))) {
|
|
375
|
-
throw new Error('`binary_files` must be a map');
|
|
376
|
-
}
|
|
377
|
-
if (body.binary_paths !== undefined && !Array.isArray(body.binary_paths)) {
|
|
378
|
-
throw new Error('`binary_paths` must be an array');
|
|
379
|
-
}
|
|
380
|
-
for (const field of ['function_entries', 'function_paths', 'transform_entries']) {
|
|
381
|
-
if (body[field] !== undefined && !Array.isArray(body[field])) throw new Error(`\`${field}\` must be an array`);
|
|
382
|
-
for (const rel of body[field] || []) assertSourcePath(rel);
|
|
383
|
-
}
|
|
384
|
-
if ((body.operation || 'project') === 'project') {
|
|
385
|
-
if (typeof body.project_id !== 'string' || !body.project_id.trim()) throw new Error('`project_id` is required');
|
|
386
|
-
if (typeof body.build_id !== 'string' || !body.build_id.trim()) throw new Error('`build_id` is required');
|
|
387
|
-
}
|
|
388
|
-
const textPaths = Object.keys(body.files);
|
|
389
|
-
const binaryFilePaths = Object.keys(body.binary_files || {});
|
|
390
|
-
const binaryPaths = body.binary_paths || [];
|
|
391
|
-
const paths = [...new Set([...textPaths, ...binaryFilePaths, ...binaryPaths])];
|
|
392
|
-
if (paths.length > MAX_FILE_COUNT) throw codedError('SOURCE_LIMIT', `too many files: ${paths.length} > ${MAX_FILE_COUNT}`);
|
|
393
|
-
for (const rel of paths) assertSourcePath(rel);
|
|
394
|
-
for (const rel of binaryFilePaths) {
|
|
395
|
-
if (Object.prototype.hasOwnProperty.call(body.files, rel)) throw codedError('INVALID_SOURCE', `path is both text and binary: ${rel}`);
|
|
396
|
-
}
|
|
397
|
-
for (const rel of binaryPaths) {
|
|
398
|
-
if (typeof rel !== 'string') throw codedError('INVALID_SOURCE_PATH', `invalid source path: ${JSON.stringify(rel)}`);
|
|
399
|
-
if (Object.prototype.hasOwnProperty.call(body.files, rel) || Object.prototype.hasOwnProperty.call(body.binary_files || {}, rel)) {
|
|
400
|
-
throw codedError('INVALID_SOURCE', `path is both inline and withheld binary: ${rel}`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
let totalBytes = 0;
|
|
404
|
-
for (const [rel, content] of Object.entries(body.files)) {
|
|
405
|
-
if (typeof content !== 'string') throw codedError('INVALID_SOURCE', `source file must contain text: ${rel}`);
|
|
406
|
-
totalBytes += Buffer.byteLength(content, 'utf8');
|
|
407
|
-
}
|
|
408
|
-
if (typeof body.package_json === 'string' && body.files['package.json'] !== body.package_json) totalBytes += Buffer.byteLength(body.package_json, 'utf8');
|
|
409
|
-
if (typeof body.tsconfig === 'string' && body.files['tsconfig.json'] !== body.tsconfig) totalBytes += Buffer.byteLength(body.tsconfig, 'utf8');
|
|
410
|
-
if (totalBytes > MAX_TOTAL_SOURCE_BYTES) {
|
|
411
|
-
throw codedError('SOURCE_LIMIT', `source bytes ${totalBytes} > ${MAX_TOTAL_SOURCE_BYTES}`);
|
|
412
|
-
}
|
|
413
|
-
let binaryBytes = 0;
|
|
414
|
-
for (const [rel, b64] of Object.entries(body.binary_files || {})) {
|
|
415
|
-
if (typeof b64 !== 'string') throw codedError('INVALID_SOURCE', `binary file must contain base64 text: ${rel}`);
|
|
416
|
-
binaryBytes += Buffer.from(b64, 'base64').length;
|
|
417
|
-
}
|
|
418
|
-
if (binaryBytes > MAX_BINARY_BYTES) {
|
|
419
|
-
throw codedError('BINARY_LIMIT', `binary bytes ${binaryBytes} > ${MAX_BINARY_BYTES}`);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function cleanRange(range) {
|
|
424
|
-
// `^18.2.0` → `18.2.0`; keep it installable but pin to the declared version.
|
|
425
|
-
return String(range || '').replace(/^[\^~>=<v\s]+/, '').split(/\s/)[0] || 'latest';
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/* ─── Lockfile-derived install pins (tsk_f79d71ce) ────────────────────────────
|
|
429
|
-
* THE INSTALL SPEC IS OWNED HERE, by the core, so `somewhere dev` and
|
|
430
|
-
* `somewhere deploy` derive it from the same code and cannot disagree.
|
|
431
|
-
*
|
|
432
|
-
* The bug this closes: deploy floor-pinned the declared range (`^7.9.5` → 7.9.5)
|
|
433
|
-
* while the developer's own machine had 7.18.3 installed from their lockfile, so
|
|
434
|
-
* the local and deployed entry chunks were built against different library code
|
|
435
|
-
* — measured on the 0.30.0 journey scaffold, where react-router was the only
|
|
436
|
-
* package left in the diff. "Same app, same data, same build" was not true.
|
|
437
|
-
*
|
|
438
|
-
* THE RULE, both directions:
|
|
439
|
-
* - LOCKFILE PRESENT (npm-shrinkwrap.json, else package-lock.json — npm's own
|
|
440
|
-
* precedence): a dependency installs at the version the lockfile pins, which
|
|
441
|
-
* is exactly what the developer has locally. The pin is only honoured when it
|
|
442
|
-
* PROVABLY SATISFIES the range declared in package.json, so a lockfile can
|
|
443
|
-
* only ever move the build WITHIN what package.json already allows; an
|
|
444
|
-
* out-of-sync lockfile falls back to the floor-pin rather than installing
|
|
445
|
-
* something package.json contradicts.
|
|
446
|
-
* - NO LOCKFILE: unchanged, and this is the documented rule — third-party deps
|
|
447
|
-
* FLOOR-PIN the declared range (`^7.9.5` → 7.9.5), first-party
|
|
448
|
-
* `@somewhere-tech/*` pass the range through verbatim so npm resolves the
|
|
449
|
-
* latest satisfying version. A project with no lockfile resolves today
|
|
450
|
-
* exactly as it did before this change (rule 9).
|
|
451
|
-
*
|
|
452
|
-
* WHY `@somewhere-tech/*` IGNORES THE LOCKFILE TOO: pfb_f607d6b27453. Our own
|
|
453
|
-
* scaffold + `npm install` writes a lockfile within seconds of `somewhere init`,
|
|
454
|
-
* so honouring it for first-party packages would re-freeze every scaffold on the
|
|
455
|
-
* SDK version it was born with and published SDK fixes would never reach new
|
|
456
|
-
* deploys — the exact harm the range passthrough exists to prevent. Right after
|
|
457
|
-
* an install the locked version IS the latest in range, so the two agree; they
|
|
458
|
-
* only part as the lockfile ages, and that is the case where the platform's own
|
|
459
|
-
* package must move.
|
|
460
|
-
*
|
|
461
|
-
* BOUNDARY, stated so nobody reads more into this than it does: this pins the
|
|
462
|
-
* dependencies the core decides to install — the ones declared in the project's
|
|
463
|
-
* package.json. Their TRANSITIVE dependencies are still resolved by npm from the
|
|
464
|
-
* parents' ranges. Pinning the whole locked closure was considered and rejected
|
|
465
|
-
* for now: an explicit exact spec for every transitive can trip ERESOLVE peer
|
|
466
|
-
* conflicts and platform-specific optional binaries, which would turn a build
|
|
467
|
-
* that works today into a refused one (rule 9 — tighten incrementally). If a
|
|
468
|
-
* transitive drift is ever measured, that is the next step, not a redesign.
|
|
469
|
-
*
|
|
470
|
-
* Prefer-baked resolution is deliberately NOT changed: which packages come from
|
|
471
|
-
* the baked image, and the React-19 symlink hot path, still key off the DECLARED
|
|
472
|
-
* RANGE. Making the baked decision exact-match would cold-install React on every
|
|
473
|
-
* lockfile project (tsk_2553662f's 600s timeout) and would re-open the very
|
|
474
|
-
* divergence tsk_0312cf17 closed by pinning the local loop to the image's own
|
|
475
|
-
* React set. Aligning the local loop with the rest of the baked set is the CLI's
|
|
476
|
-
* half of this, not the compiler's.
|
|
477
|
-
*/
|
|
478
|
-
|
|
479
|
-
/** npm's own lockfile precedence: a shrinkwrap wins over a package-lock. */
|
|
480
|
-
const LOCKFILE_NAMES = ['npm-shrinkwrap.json', 'package-lock.json'];
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Versions an npm lockfile pins for the project's TOP-LEVEL packages.
|
|
484
|
-
*
|
|
485
|
-
* Only `node_modules/<name>` entries count: a nested `node_modules/a/node_modules/b`
|
|
486
|
-
* path is a transitive's private copy, not the version the project itself
|
|
487
|
-
* resolves. Link entries (workspaces, `file:` deps) carry no registry version and
|
|
488
|
-
* are skipped. Handles lockfileVersion 2/3 (`packages`) and 1 (`dependencies`).
|
|
489
|
-
* Malformed input yields an empty map — never a throw; a lockfile we cannot read
|
|
490
|
-
* degrades to today's floor-pin.
|
|
491
|
-
*/
|
|
492
|
-
function lockfileVersions(text) {
|
|
493
|
-
const out = new Map();
|
|
494
|
-
if (typeof text !== 'string' || !text.trim()) return out;
|
|
495
|
-
let lock;
|
|
496
|
-
try { lock = JSON.parse(text); } catch { return out; }
|
|
497
|
-
if (!lock || typeof lock !== 'object') return out;
|
|
498
|
-
const packages = lock.packages && typeof lock.packages === 'object' ? lock.packages : null;
|
|
499
|
-
if (packages) {
|
|
500
|
-
for (const [entryPath, entry] of Object.entries(packages)) {
|
|
501
|
-
if (!entryPath.startsWith('node_modules/')) continue;
|
|
502
|
-
const name = entryPath.slice('node_modules/'.length);
|
|
503
|
-
if (name.includes('/node_modules/')) continue;
|
|
504
|
-
if (!entry || typeof entry !== 'object' || entry.link) continue;
|
|
505
|
-
if (typeof entry.version !== 'string' || !entry.version) continue;
|
|
506
|
-
out.set(name, entry.version);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
const v1 = lock.dependencies && typeof lock.dependencies === 'object' ? lock.dependencies : null;
|
|
510
|
-
if (v1) {
|
|
511
|
-
for (const [name, entry] of Object.entries(v1)) {
|
|
512
|
-
if (out.has(name)) continue;
|
|
513
|
-
if (!entry || typeof entry !== 'object') continue;
|
|
514
|
-
if (typeof entry.version !== 'string' || !entry.version) continue;
|
|
515
|
-
if (/^(?:file|link|git\+|https?):/.test(entry.version)) continue;
|
|
516
|
-
out.set(name, entry.version);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
return out;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* The one derivation of what to hand `npm install` for a single dependency.
|
|
524
|
-
* PURE — `satisfies(version, range)` is passed in (semver is host-loaded) and
|
|
525
|
-
* must return TRUE only when it can prove the version is inside the range;
|
|
526
|
-
* anything it cannot prove falls back to the floor-pin, i.e. today's behavior.
|
|
527
|
-
*
|
|
528
|
-
* Exported so the fixture can pin the rule in BOTH directions without a build.
|
|
529
|
-
*/
|
|
530
|
-
function resolveInstallSpec({ name, range, lockedVersion, satisfies }) {
|
|
531
|
-
// First-party platform packages: declared range verbatim, lockfile ignored.
|
|
532
|
-
// See the block comment above (pfb_f607d6b27453).
|
|
533
|
-
if (name.startsWith('@somewhere-tech/')) {
|
|
534
|
-
return `${name}@${String(range || 'latest').trim() || 'latest'}`;
|
|
535
|
-
}
|
|
536
|
-
if (lockedVersion && typeof satisfies === 'function') {
|
|
537
|
-
let inRange = false;
|
|
538
|
-
try { inRange = satisfies(lockedVersion, range) === true; } catch { inRange = false; }
|
|
539
|
-
if (inRange) return `${name}@${lockedVersion}`;
|
|
540
|
-
}
|
|
541
|
-
return `${name}@${cleanRange(range)}`;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* The project's locked versions, read from the materialized build root. Both
|
|
546
|
-
* hosts land here: the container materializes every uploaded file (the lockfile
|
|
547
|
-
* included) into the build root, and the CLI's build root is the project itself.
|
|
548
|
-
* A missing or unreadable lockfile is not an error — it is the documented
|
|
549
|
-
* no-lockfile path.
|
|
550
|
-
*/
|
|
551
|
-
function readLockedVersions(root) {
|
|
552
|
-
for (const name of LOCKFILE_NAMES) {
|
|
553
|
-
let text;
|
|
554
|
-
try { text = fs.readFileSync(path.join(root, name), 'utf8'); } catch { continue; }
|
|
555
|
-
const versions = lockfileVersions(text);
|
|
556
|
-
if (versions.size) return versions;
|
|
557
|
-
}
|
|
558
|
-
return new Map();
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Is `name` resolvable the way ESBUILD will resolve it — i.e. as a direct child
|
|
563
|
-
* package of one of the exact search dirs (the per-build install + the baked
|
|
564
|
-
* image set, mirroring esbuild's nodePaths)? We deliberately do NOT use Node's
|
|
565
|
-
* require.resolve here: its `paths` option walks UP ancestor directories, so in
|
|
566
|
-
* the monorepo it finds deps in a parent node_modules that esbuild's strict
|
|
567
|
-
* nodePaths can't see — over-reporting "resolvable" and skipping an install
|
|
568
|
-
* esbuild then needs. An isolated container has no such ancestor; this fs check
|
|
569
|
-
* is correct in both environments.
|
|
570
|
-
*/
|
|
571
|
-
function isResolvable(name, searchDirs) {
|
|
572
|
-
return searchDirs.some((dir) => fs.existsSync(path.join(dir, name, 'package.json')));
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Build-toolchain packages that legacy scaffolds declare under `dependencies`
|
|
576
|
-
// (CRA puts react-scripts there by default). The browser bundle never imports
|
|
577
|
-
// them, and installing react-scripts alone drags in a ~100MB tree that can
|
|
578
|
-
// blow the per-build install timeout — the platform IS the build toolchain.
|
|
579
|
-
// (tsk_4d9c865a)
|
|
580
|
-
const TOOLCHAIN_DEPS = new Set(['react-scripts', 'react-app-rewired', 'craco', '@craco/craco', 'vite', 'webpack', 'parcel']);
|
|
581
|
-
|
|
582
|
-
/* ─── Phantom-import detection (tsk_8e460b69) ─────────────────────────────────
|
|
583
|
-
* esbuild 0.24 tree-shakes a bare import whose named bindings are ALL unused
|
|
584
|
-
* before it attempts to resolve the module. An `import { nothing } from
|
|
585
|
-
* 'missing-pkg'` where `nothing` is never referenced silently succeeds — no
|
|
586
|
-
* "Could not resolve" error and no warning. The customer's deploy
|
|
587
|
-
* goes live; if that code path ever executes in the browser it fails at runtime.
|
|
588
|
-
*
|
|
589
|
-
* This scanner runs AFTER the esbuild build succeeds and catches that class:
|
|
590
|
-
* every bare specifier found in the frontend source that is NOT resolvable
|
|
591
|
-
* through the same set of sources esbuild would use (per-build install, baked
|
|
592
|
-
* image, React-19 baked set, NODE_POLYFILLS aliases) is surfaced as a warning.
|
|
593
|
-
*
|
|
594
|
-
* RULE 9 — WARN-FIRST: never a build failure. A phantom import the customer
|
|
595
|
-
* hasn't reached yet doesn't break today's page. The warning names the package
|
|
596
|
-
* and the file so the developer or their agent can add it to package.json.
|
|
597
|
-
*
|
|
598
|
-
* GRANDFATHER: the full resolvability chain (build root → IMAGE_NODE_MODULES →
|
|
599
|
-
* REACT19_NODE_MODULES → NODE_POLYFILLS → node: prefix → relative/URL paths)
|
|
600
|
-
* mirrors exactly what esbuild can resolve, so a working import NEVER warns.
|
|
601
|
-
* Platform-provided specifiers (PLATFORM_MODULE_ROOTS) are exempt on top of
|
|
602
|
-
* that: they are not npm packages, so no resolution chain would ever find them.
|
|
603
|
-
*/
|
|
604
|
-
|
|
605
|
-
/**
|
|
606
|
-
* Extract bare import/export specifiers from JS-family source.
|
|
607
|
-
* "Bare" = not relative (`./`, `../`), not root-anchored (`/`), not a URL.
|
|
608
|
-
* Handles static import, re-export, bare side-effect import, and dynamic import().
|
|
609
|
-
*/
|
|
610
|
-
function extractBareSpecifiers(content) {
|
|
611
|
-
if (typeof content !== 'string') return [];
|
|
612
|
-
const out = new Set();
|
|
613
|
-
const re = /(?:\bimport\b[^'";]*?\bfrom\s*|\bexport\b[^'";]*?\bfrom\s*|\bimport\s*\(\s*|\bimport\s*)(['"])([^'"]+)\1/g;
|
|
614
|
-
let m;
|
|
615
|
-
while ((m = re.exec(content)) !== null) {
|
|
616
|
-
const spec = m[2];
|
|
617
|
-
if (!spec) continue;
|
|
618
|
-
if (spec.startsWith('./') || spec.startsWith('../') || spec.startsWith('/')) continue;
|
|
619
|
-
if (/^[a-z][a-z0-9+.-]*:/i.test(spec)) continue; // http:, data:, blob:, node:, virtual:, …
|
|
620
|
-
out.add(spec);
|
|
621
|
-
}
|
|
622
|
-
return [...out];
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
/**
|
|
626
|
-
* Return the root package name for a specifier: `@scope/pkg/subpath` → `@scope/pkg`,
|
|
627
|
-
* `pkg/subpath` → `pkg`, `pkg` → `pkg`.
|
|
628
|
-
*/
|
|
629
|
-
function specRootPkg(spec) {
|
|
630
|
-
return spec.startsWith('@')
|
|
631
|
-
? spec.split('/').slice(0, 2).join('/')
|
|
632
|
-
: spec.split('/')[0];
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* Root packages the PLATFORM provides — derived from typed-functions'
|
|
637
|
-
* PLATFORM_MODULES, which is also what builds the virtual-module resolver, so
|
|
638
|
-
* this can never drift into a stale hand list.
|
|
639
|
-
*
|
|
640
|
-
* These are not npm packages. Reporting one as "not in your package.json" told
|
|
641
|
-
* a developer that the `somewhere/db` import our own managed-schema docs handed
|
|
642
|
-
* them would fail to load in production, and the remediation it offered —
|
|
643
|
-
* `npm install somewhere` — installs an unrelated package (tsk_53badecfb7).
|
|
644
|
-
*/
|
|
645
|
-
const PLATFORM_MODULE_ROOTS = new Set(typedFunctions.PLATFORM_MODULES.map(specRootPkg));
|
|
646
|
-
|
|
647
|
-
/* ─── Dependency review (PR1, tsk_35befef9 / npm supply-chain) ────────────────
|
|
648
|
-
* After deps are installed (with --ignore-scripts), walk the customer's RESOLVED
|
|
649
|
-
* dependency tree and report what actually shipped: package count, lifecycle
|
|
650
|
-
* scripts that were SKIPPED, native modules, and any resolved version that
|
|
651
|
-
* matches a known-compromised release. Surfaced at deploy as the "dependency
|
|
652
|
-
* review" block + (for a known-bad hit) a non-blocking warning.
|
|
653
|
-
*
|
|
654
|
-
* RULE 9 — this is WARN-FIRST: we report, we never refuse the build. Promotion
|
|
655
|
-
* of any signal to an actual block is a separate, gated step (PR4).
|
|
656
|
-
*
|
|
657
|
-
* No network: reads only the installed package.json files on the build fs. The
|
|
658
|
-
* AUTHORITATIVE live advisory scan (OSV/npm-advisory) ships with the R2
|
|
659
|
-
* dependency cache (PR2), running on cache-entry; this is the certain, feed-free
|
|
660
|
-
* signal + a small hand-verified known-bad backstop so a famous worm can't slip
|
|
661
|
-
* through before that feed lands.
|
|
662
|
-
*
|
|
663
|
-
* Scope = the customer's installed tree (build-root node_modules) — the long
|
|
664
|
-
* tail npm pulled in. The pre-baked image deps (our toolchain + react) are our
|
|
665
|
-
* own trusted pins, not the customer's supply-chain surface, so they're excluded.
|
|
666
|
-
*/
|
|
667
|
-
|
|
668
|
-
// Hand-verified backstop of CONFIRMED-compromised exact versions. NOT the
|
|
669
|
-
// authoritative source (PR2's live OSV scan is) — every entry is an exact
|
|
670
|
-
// name@version confirmed across independent advisories. Rule 7: no guessed or
|
|
671
|
-
// approximate entry, because a wrong known-bad becomes a false BLOCK the moment
|
|
672
|
-
// these promote past warn-first.
|
|
673
|
-
//
|
|
674
|
-
// Sept 8 2025 "crypto-clipper" cluster — chalk/debug/ansi maintainer phished
|
|
675
|
-
// (npmjs.help 2FA-reset lure); injected a browser wallet-drainer. Sources:
|
|
676
|
-
// Sonatype, Wiz, Semgrep, Vercel, Palo Alto Unit 42.
|
|
677
|
-
const KNOWN_BAD_VERSIONS = {
|
|
678
|
-
debug: ['4.4.2'], chalk: ['5.6.1'], 'ansi-styles': ['6.2.2'], 'ansi-regex': ['6.2.1'],
|
|
679
|
-
'strip-ansi': ['7.1.1'], 'wrap-ansi': ['9.0.1'], 'supports-color': ['10.2.1'],
|
|
680
|
-
'supports-hyperlinks': ['4.1.1'], color: ['5.0.1'], 'color-name': ['2.0.1'],
|
|
681
|
-
'color-convert': ['3.1.1'], 'color-string': ['2.1.1'], 'has-ansi': ['6.0.1'],
|
|
682
|
-
'slice-ansi': ['7.1.1'], 'chalk-template': ['1.1.1'], backslash: ['0.2.1'],
|
|
683
|
-
'is-arrayish': ['0.3.3'], 'error-ex': ['1.3.3'], 'simple-swizzle': ['0.2.3'],
|
|
684
|
-
'proto-tinker-wc': ['0.1.87'], duckdb: ['1.3.3'], '@duckdb/node-api': ['1.3.3'],
|
|
685
|
-
'@duckdb/node-bindings': ['1.3.3'], '@duckdb/duckdb-wasm': ['1.29.2'],
|
|
686
|
-
};
|
|
687
|
-
|
|
688
|
-
const MAX_REVIEW_PACKAGES = 5000; // pathological-tree guard
|
|
689
|
-
const NATIVE_BUILD_TOOLS = /node-gyp|node-pre-gyp|prebuild|prebuildify|cmake-js/;
|
|
690
|
-
|
|
691
|
-
// Walk a node_modules dir → {name, version, dir, pkg} per installed package.
|
|
692
|
-
// Handles @scope packages and nested node_modules (npm dedupe conflicts).
|
|
693
|
-
function* walkInstalled(nmDir, depth) {
|
|
694
|
-
if (depth > 6 || !fs.existsSync(nmDir)) return;
|
|
695
|
-
let entries;
|
|
696
|
-
try { entries = fs.readdirSync(nmDir, { withFileTypes: true }); } catch { return; }
|
|
697
|
-
for (const e of entries) {
|
|
698
|
-
if (e.name.startsWith('.')) continue;
|
|
699
|
-
const full = path.join(nmDir, e.name);
|
|
700
|
-
if (e.name.startsWith('@')) {
|
|
701
|
-
let scoped;
|
|
702
|
-
try { scoped = fs.readdirSync(full, { withFileTypes: true }); } catch { continue; }
|
|
703
|
-
for (const s of scoped) {
|
|
704
|
-
if (s.name.startsWith('.')) continue;
|
|
705
|
-
yield* emitInstalledPkg(path.join(full, s.name), depth);
|
|
706
|
-
}
|
|
707
|
-
} else {
|
|
708
|
-
yield* emitInstalledPkg(full, depth);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
function* emitInstalledPkg(dir, depth) {
|
|
713
|
-
let pkg = null;
|
|
714
|
-
try { pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')); } catch { /* not a package dir */ }
|
|
715
|
-
if (pkg && typeof pkg.name === 'string') yield { name: pkg.name, version: pkg.version || '0.0.0', dir, pkg };
|
|
716
|
-
yield* walkInstalled(path.join(dir, 'node_modules'), depth + 1);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function reviewDependencies(root, pkg) {
|
|
720
|
-
const directDeps = (pkg && pkg.dependencies) || {};
|
|
721
|
-
const review = {
|
|
722
|
-
directDependencies: Object.keys(directDeps).length,
|
|
723
|
-
scanned: 0,
|
|
724
|
-
installScriptsExecuted: 0, // always 0 — --ignore-scripts
|
|
725
|
-
installScriptsSkipped: [], // [{name, version, hooks}]
|
|
726
|
-
nativeModules: [], // [{name, version}]
|
|
727
|
-
knownBad: [], // [{name, version, severity, incident}]
|
|
728
|
-
packages: [], // [{name, version}] — resolved set, for the
|
|
729
|
-
// worker's live OSV advisory scan (PR4). Capped.
|
|
730
|
-
};
|
|
731
|
-
const seen = new Set();
|
|
732
|
-
for (const { name, version, dir, pkg: p } of walkInstalled(path.join(root, 'node_modules'), 0)) {
|
|
733
|
-
const key = `${name}@${version}`;
|
|
734
|
-
if (seen.has(key)) continue;
|
|
735
|
-
seen.add(key);
|
|
736
|
-
if (seen.size > MAX_REVIEW_PACKAGES) break;
|
|
737
|
-
review.scanned++;
|
|
738
|
-
review.packages.push({ name, version });
|
|
739
|
-
|
|
740
|
-
const scripts = (p && p.scripts) || {};
|
|
741
|
-
const hooks = ['preinstall', 'install', 'postinstall'].filter((h) => typeof scripts[h] === 'string' && scripts[h].trim());
|
|
742
|
-
if (hooks.length) review.installScriptsSkipped.push({ name, version, hooks });
|
|
743
|
-
|
|
744
|
-
const isNative = (p && p.gypfile === true)
|
|
745
|
-
|| fs.existsSync(path.join(dir, 'binding.gyp'))
|
|
746
|
-
|| hooks.some((h) => NATIVE_BUILD_TOOLS.test(scripts[h] || ''));
|
|
747
|
-
if (isNative) review.nativeModules.push({ name, version });
|
|
748
|
-
|
|
749
|
-
const bad = KNOWN_BAD_VERSIONS[name];
|
|
750
|
-
if (bad && bad.includes(version)) {
|
|
751
|
-
review.knownBad.push({ name, version, severity: 'critical', incident: 'confirmed-compromised npm release' });
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
return review;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/* ─── Manifest path (tsk_118c1ecc) — direct-to-storage artifact upload ──────
|
|
758
|
-
* A heavy compiled result (~36MB for project `tools`) cannot safely ride back
|
|
759
|
-
* inline. When the request carries `artifact_upload` {url, token}, the
|
|
760
|
-
* container PUTs every chunk to the worker's /v1/compile-artifacts route
|
|
761
|
-
* (gated by the scoped single-build token — same pattern as the npm-proxy
|
|
762
|
-
* per-build token) and the response carries only a MANIFEST: chunk names,
|
|
763
|
-
* sizes, sha256 hashes. No chunk bodies. */
|
|
764
|
-
|
|
765
|
-
/** Throw an Error carrying a machine-readable `code` the worker branches on. */
|
|
766
|
-
function codedError(code, message) {
|
|
767
|
-
const err = new Error(message);
|
|
768
|
-
err.code = code;
|
|
769
|
-
return err;
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
function artifactDescriptor(pathname, kind, text) {
|
|
773
|
-
const bytes = Buffer.from(text, 'utf8');
|
|
774
|
-
const mime = pathname.endsWith('.css')
|
|
775
|
-
? 'text/css; charset=utf-8'
|
|
776
|
-
: 'application/javascript; charset=utf-8';
|
|
777
|
-
return {
|
|
778
|
-
path: pathname,
|
|
779
|
-
kind,
|
|
780
|
-
bytes: bytes.length,
|
|
781
|
-
sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
782
|
-
mime,
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function inlineResultBodyBytes(result) {
|
|
787
|
-
let bytes = 0;
|
|
788
|
-
for (const text of Object.values(result.chunks || {})) bytes += Buffer.byteLength(text, 'utf8');
|
|
789
|
-
for (const artifact of Object.values(result.functions || {})) bytes += Buffer.byteLength(artifact.js, 'utf8');
|
|
790
|
-
for (const artifact of Object.values(result.transforms || {})) bytes += Buffer.byteLength(artifact.js, 'utf8');
|
|
791
|
-
return bytes;
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function enforceInlineResultCap(body, result) {
|
|
795
|
-
const cap = body.inline_result_cap_bytes;
|
|
796
|
-
if (!Number.isFinite(cap) || cap <= 0) return;
|
|
797
|
-
const inlineBytes = inlineResultBodyBytes(result);
|
|
798
|
-
const serializedBytes = Buffer.byteLength(JSON.stringify(result), 'utf8');
|
|
799
|
-
if (serializedBytes <= cap) return;
|
|
800
|
-
const mb = (serializedBytes / (1024 * 1024)).toFixed(1);
|
|
801
|
-
const inlineMb = (inlineBytes / (1024 * 1024)).toFixed(1);
|
|
802
|
-
const capMb = (cap / (1024 * 1024)).toFixed(0);
|
|
803
|
-
throw codedError(
|
|
804
|
-
'RESULT_TOO_LARGE',
|
|
805
|
-
`compiled result is ${mb}MB (${inlineMb}MB inline bodies) across ${result.artifact_manifest.artifacts.length} artifacts, above the ${capMb}MB limit`,
|
|
806
|
-
);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function resolvedDependencyDigest(review) {
|
|
810
|
-
const packages = [...(review && review.packages || [])]
|
|
811
|
-
.map((item) => `${item.name}@${item.version}`)
|
|
812
|
-
.sort();
|
|
813
|
-
return crypto.createHash('sha256').update(packages.join('\n')).digest('hex');
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
function sourceLoader(filename) {
|
|
817
|
-
if (/\.tsx$/i.test(filename)) return 'tsx';
|
|
818
|
-
if (/\.(?:ts|mts|cts)$/i.test(filename)) return 'ts';
|
|
819
|
-
if (/\.jsx$/i.test(filename)) return 'jsx';
|
|
820
|
-
if (/\.(?:js|mjs|cjs)$/i.test(filename)) return 'js';
|
|
821
|
-
if (/\.json$/i.test(filename)) return 'json';
|
|
822
|
-
return null;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
function resolveLocalFunctionImport(root, spec) {
|
|
826
|
-
if (typeof spec !== 'string' || /^(?:node|cloudflare|workerd):/.test(spec)) return null;
|
|
827
|
-
if (spec.startsWith('.') || /^[a-z][a-z0-9+.-]*:/i.test(spec)) return null;
|
|
828
|
-
const base = path.resolve(root, spec.replace(/^\/+/, ''));
|
|
829
|
-
if (base !== root && !base.startsWith(root + path.sep)) return null;
|
|
830
|
-
const candidates = [
|
|
831
|
-
base,
|
|
832
|
-
...['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json'].map((ext) => base + ext),
|
|
833
|
-
...['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json'].map((ext) => path.join(base, 'index' + ext)),
|
|
834
|
-
];
|
|
835
|
-
return candidates.find((candidate) => {
|
|
836
|
-
try { return fs.statSync(candidate).isFile(); } catch { return false; }
|
|
837
|
-
}) || null;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
function functionLocalImportPlugin(root) {
|
|
841
|
-
return {
|
|
842
|
-
name: 'somewhere-function-local-root',
|
|
843
|
-
setup(build) {
|
|
844
|
-
build.onResolve({ filter: /.*/ }, (args) => {
|
|
845
|
-
if (args.kind === 'entry-point') return null;
|
|
846
|
-
if (/^(?:node|cloudflare|workerd):/.test(args.path)) return { path: args.path, external: true };
|
|
847
|
-
const local = resolveLocalFunctionImport(root, args.path);
|
|
848
|
-
return local ? { path: local } : null;
|
|
849
|
-
});
|
|
850
|
-
},
|
|
851
|
-
};
|
|
852
|
-
}
|
|
853
|
-
/* ══════════════════════════════════════════════════════════════════════════
|
|
854
|
-
* ENVIRONMENT-BOUND — everything that needs to know where the baked toolchain
|
|
855
|
-
* and dependency trees live, how an install runs, and what stamps this build.
|
|
856
|
-
* ══════════════════════════════════════════════════════════════════════════ */
|
|
857
|
-
|
|
858
|
-
/**
|
|
859
|
-
* Build a compiler instance bound to one host environment.
|
|
860
|
-
* @param {object} host — see the file header for the complete contract.
|
|
861
|
-
*/
|
|
862
|
-
/**
|
|
863
|
-
* Build-CONFIG files, which are never in the browser graph.
|
|
864
|
-
*
|
|
865
|
-
* The platform IS the bundler, so a project's vite/rollup/webpack/postcss/
|
|
866
|
-
* tailwind config is never executed by anything the compiler emits. Scanning
|
|
867
|
-
* them for phantom imports produced a warning whose own premise was false —
|
|
868
|
-
* "if that code path runs, your app will fail to load it", for a code path
|
|
869
|
-
* that cannot run — on our OWN init scaffold, whose vite.config.ts imports
|
|
870
|
-
* vite and @vitejs/plugin-react (tsk_424174be).
|
|
871
|
-
*/
|
|
872
|
-
function isBuildConfigFile(filePath) {
|
|
873
|
-
const base = filePath.split('/').pop() || '';
|
|
874
|
-
return /^(?:vite|vitest|rollup|webpack|next|nuxt|svelte|astro|remix|tailwind|postcss|babel|jest|playwright|cypress|eslint|prettier|stylelint)\.config\.[cm]?[jt]sx?$/i.test(base);
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/* ─── devDependencies the app's own source actually imports (tsk_2a7c6b33) ────
|
|
878
|
-
* The install is `dependencies`-only BY DESIGN — the platform is the build
|
|
879
|
-
* toolchain, so vite/typescript/@vitejs/* have no business in a per-build
|
|
880
|
-
* install. That was always the stated rule, and `missing` always honoured it,
|
|
881
|
-
* but `npm install <specs>` runs in a build root holding the customer's whole
|
|
882
|
-
* package.json and npm reifies the FULL declared tree from it, devDependencies
|
|
883
|
-
* included. Measured on our own `somewhere init` scaffold: 74 packages / 72 MB
|
|
884
|
-
* with the dev closure (vite, rollup, @babel/*, esbuild + its platform binary,
|
|
885
|
-
* typescript, @types/*), 8 packages / 13 MB without it. Every scaffold deploy
|
|
886
|
-
* has been paying for a toolchain the bundle never imports, and on a slow or
|
|
887
|
-
* churning instance that is what puts an install over the 300 s build budget.
|
|
888
|
-
* `--omit=dev` on the npm side closes it (server.js).
|
|
889
|
-
*
|
|
890
|
-
* RULE 9 — THE CARVE-OUT THAT KEEPS WORKING CODE WORKING: because the dev
|
|
891
|
-
* closure landed incidentally, a project that IMPORTS a dev-declared package
|
|
892
|
-
* from its own source has always had that import resolve. Dropping the dev
|
|
893
|
-
* tree wholesale would turn those builds into "Could not resolve" failures —
|
|
894
|
-
* the exact class rule 9 forbids. So a devDependency the source imports is
|
|
895
|
-
* installed EXPLICITLY, by name, and nothing that builds today stops building.
|
|
896
|
-
* Only the unimported remainder — the toolchain — goes away.
|
|
897
|
-
*
|
|
898
|
-
* BUILD-CONFIG FILES ARE EXCLUDED, and that exclusion is the whole point: the
|
|
899
|
-
* scaffold's own vite.config.ts imports `vite` and `@vitejs/plugin-react`, and
|
|
900
|
-
* the platform never executes it (isBuildConfigFile, tsk_424174be). Counting it
|
|
901
|
-
* as an import would reinstall the toolchain and buy nothing.
|
|
902
|
-
*/
|
|
903
|
-
function importedDevDeps(pkg, files) {
|
|
904
|
-
const declared = (pkg && pkg.devDependencies) || {};
|
|
905
|
-
const names = Object.keys(declared);
|
|
906
|
-
if (!names.length || !files || typeof files !== 'object') return [];
|
|
907
|
-
const declaredSet = new Set(names);
|
|
908
|
-
const hit = new Set();
|
|
909
|
-
for (const [filePath, content] of Object.entries(files)) {
|
|
910
|
-
if (typeof content !== 'string') continue;
|
|
911
|
-
if (!/\.(tsx?|jsx?|mjs|cjs)$/i.test(filePath)) continue;
|
|
912
|
-
if (isBuildConfigFile(filePath)) continue;
|
|
913
|
-
for (const spec of extractBareSpecifiers(content)) {
|
|
914
|
-
const root = specRootPkg(spec);
|
|
915
|
-
if (declaredSet.has(root)) hit.add(root);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
return [...hit];
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function createCompileCore(host) {
|
|
922
|
-
if (!host || typeof host !== 'object') throw new Error('createCompileCore(host) requires a host');
|
|
923
|
-
for (const required of ['imageNodeModules', 'requireImage', 'installPackages']) {
|
|
924
|
-
if (host[required] === undefined || host[required] === null) {
|
|
925
|
-
throw new Error(`createCompileCore(host): host.${required} is required`);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
// One dir or several, in search order. The container has exactly one (its
|
|
929
|
-
// baked image tree); the CLI's local dev loop has up to two (the project's
|
|
930
|
-
// own node_modules first, then its managed cache), which is why this is a
|
|
931
|
-
// list rather than the single path the container needed.
|
|
932
|
-
// esbuild IS the compiler: same version in, same bytes out. A host may hand
|
|
933
|
-
// us a different BUILD of that exact version (the CLI uses esbuild-wasm
|
|
934
|
-
// 0.24.0, which the parity fixture proves emits identical output to the
|
|
935
|
-
// container's native 0.24.0) but never a different version — the compiler
|
|
936
|
-
// stamp carries esbuild.version and the vendor step refuses a pin mismatch.
|
|
937
|
-
const esbuild = host.esbuild || require('esbuild');
|
|
938
|
-
const IMAGE_NODE_MODULES = Array.isArray(host.imageNodeModules)
|
|
939
|
-
? host.imageNodeModules.filter(Boolean)
|
|
940
|
-
: [host.imageNodeModules];
|
|
941
|
-
if (!IMAGE_NODE_MODULES.length) throw new Error('createCompileCore(host): host.imageNodeModules is empty');
|
|
942
|
-
const REACT19_NODE_MODULES = host.react19NodeModules || null;
|
|
943
|
-
const imageRequire = host.requireImage;
|
|
944
|
-
const tw4Require = host.requireTw4 || (() => { throw new Error('this host has no Tailwind v4 engine'); });
|
|
945
|
-
const stamp = host.stamp || {};
|
|
946
|
-
|
|
947
|
-
function compilerStamp(sourceDigest) {
|
|
948
|
-
return {
|
|
949
|
-
contract: COMPILER_CONTRACT,
|
|
950
|
-
esbuild: esbuild.version,
|
|
951
|
-
source: stamp.source,
|
|
952
|
-
toolchain: stamp.toolchain,
|
|
953
|
-
...(sourceDigest ? { source_digest: sourceDigest } : {}),
|
|
954
|
-
};
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// semver is lazily required from the baked image and backs ensureDeps'
|
|
958
|
-
// version-aware resolution. If it somehow can't load, getSemver returns null and
|
|
959
|
-
// bakedSatisfies degrades to "does not satisfy" → per-build install (slower but
|
|
960
|
-
// correct) — never a crash, and never a false baked match.
|
|
961
|
-
let _semver;
|
|
962
|
-
function getSemver() {
|
|
963
|
-
if (_semver !== undefined) return _semver;
|
|
964
|
-
try { _semver = imageRequire('semver'); } catch { _semver = null; }
|
|
965
|
-
return _semver;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
/**
|
|
969
|
-
* The prefer-baked-but-respect-the-pin rule for ONE baked dependency: return
|
|
970
|
-
* true only when the image's pre-baked copy of `name` satisfies the project's
|
|
971
|
-
* requested `range`. When it returns false the caller installs the project's
|
|
972
|
-
* declared version per-build into the build root, and esbuild resolves the build
|
|
973
|
-
* root's node_modules BEFORE the baked nodePaths — so the project's version wins
|
|
974
|
-
* and a baked dep is never forced onto an app that pinned another.
|
|
975
|
-
*
|
|
976
|
-
* Bias is deliberate and one-directional: a FALSE POSITIVE (claiming baked
|
|
977
|
-
* satisfies when it doesn't) would ship the wrong version to the customer, so we
|
|
978
|
-
* never risk it. Anything we cannot positively prove satisfies — a missing baked
|
|
979
|
-
* version, a range semver can't parse (`latest`, git/alias specs), or semver
|
|
980
|
-
* being unavailable — returns false and falls back to a per-build install, which
|
|
981
|
-
* is correct, just slower. A false NEGATIVE only costs an unnecessary install.
|
|
982
|
-
*/
|
|
983
|
-
function bakedSatisfies(name, range, baseDir) {
|
|
984
|
-
// No explicit dir = ask the whole search path, in order. Any dir whose copy
|
|
985
|
-
// satisfies the range is a hit, because that is the copy esbuild resolves.
|
|
986
|
-
if (baseDir === undefined) return IMAGE_NODE_MODULES.some((dir) => bakedSatisfies(name, range, dir));
|
|
987
|
-
let bakedVersion;
|
|
988
|
-
try {
|
|
989
|
-
bakedVersion = JSON.parse(fs.readFileSync(path.join(baseDir, name, 'package.json'), 'utf8')).version;
|
|
990
|
-
} catch { return false; }
|
|
991
|
-
if (!bakedVersion) return false;
|
|
992
|
-
const semver = getSemver();
|
|
993
|
-
if (!semver) return false;
|
|
994
|
-
// Empty / missing range means "any version" — the baked copy is fine.
|
|
995
|
-
const wanted = (typeof range === 'string' && range.trim()) ? range.trim() : '*';
|
|
996
|
-
if (semver.validRange(wanted) === null) return false; // 'latest', git/alias specs, etc.
|
|
997
|
-
try { return semver.satisfies(bakedVersion, wanted, { includePrerelease: false }); }
|
|
998
|
-
catch { return false; }
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// Packages the React 19 isolated set provides. ensureDeps consults the baked 19
|
|
1002
|
-
// for these BEFORE deciding to install — but ONLY when the baked 18 in
|
|
1003
|
-
// IMAGE_NODE_MODULES does NOT satisfy the pin (i.e. the app actually wants 19).
|
|
1004
|
-
const REACT19_PACKAGES = new Set(['react', 'react-dom']);
|
|
1005
|
-
|
|
1006
|
-
/**
|
|
1007
|
-
* For a react/react-dom dependency that the default baked 18 does NOT satisfy:
|
|
1008
|
-
* can the baked React-19 set satisfy it? Returns the absolute dir of the baked
|
|
1009
|
-
* 19 package to symlink, or null (→ fall through to a per-build install of the
|
|
1010
|
-
* project's exact pin — the incompatible-pin path, RULE-6). The same prefer-
|
|
1011
|
-
* baked-but-respect-the-pin discipline as bakedSatisfies, just against the
|
|
1012
|
-
* isolated react19/ node_modules.
|
|
1013
|
-
*/
|
|
1014
|
-
function react19BakedDir(name, range) {
|
|
1015
|
-
if (!REACT19_PACKAGES.has(name)) return null;
|
|
1016
|
-
// A host may have no isolated React-19 set (the CLI's dep cache is one flat
|
|
1017
|
-
// tree). Then there is nothing to prefer and the pin installs normally.
|
|
1018
|
-
if (!REACT19_NODE_MODULES) return null;
|
|
1019
|
-
if (!bakedSatisfies(name, range, REACT19_NODE_MODULES)) return null;
|
|
1020
|
-
return path.join(REACT19_NODE_MODULES, name);
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
/**
|
|
1024
|
-
* Resolve the customer's `dependencies` (directive flag #1 + invariant #2:
|
|
1025
|
-
* resolved versions match local), with version-aware prefer-baked resolution.
|
|
1026
|
-
* Per dependency, in order:
|
|
1027
|
-
* 1. Already in THIS build's node_modules → nothing to do.
|
|
1028
|
-
* 2. Baked in the image AND the baked version satisfies the requested range
|
|
1029
|
-
* (bakedSatisfies) → use the baked copy, no install. This is the hot path:
|
|
1030
|
-
* react + the top long-tail libs ship in the image, so most builds do zero
|
|
1031
|
-
* `npm install` (perf) and the container barely reaches the registry (good
|
|
1032
|
-
* for the egress-locked compile container).
|
|
1033
|
-
* 3. Otherwise → `npm install` the declared version into the build root. This
|
|
1034
|
-
* covers both un-baked long-tail deps AND the case where the project pinned
|
|
1035
|
-
* a version the baked copy does NOT satisfy (step 2 returned false): the
|
|
1036
|
-
* per-build install lands in the build root, which esbuild resolves before
|
|
1037
|
-
* the baked nodePaths, so the project's pin wins.
|
|
1038
|
-
* devDependencies (tailwind, vite, etc.) are NOT installed — the build toolchain
|
|
1039
|
-
* is the container's job, not the app's. The ONE exception is a devDependency
|
|
1040
|
-
* the project's own source imports: see importedDevDeps (tsk_2a7c6b33) for why
|
|
1041
|
-
* that carve-out exists and why it is not optional.
|
|
1042
|
-
*/
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
async function ensureDeps(root, pkg, warnings, ctx, scopedProxyReady = true, sourceFiles = null) {
|
|
1046
|
-
// `dependencies`, plus any devDependency the source actually imports. npm
|
|
1047
|
-
// is told `--omit=dev` (server.js), so anything the app really needs must
|
|
1048
|
-
// be named here explicitly — which is what keeps a build that resolves a
|
|
1049
|
-
// dev-declared import today resolving it tomorrow (rule 9).
|
|
1050
|
-
const declaredDev = (pkg && pkg.devDependencies) || {};
|
|
1051
|
-
const deps = { ...((pkg && pkg.dependencies) || {}) };
|
|
1052
|
-
for (const name of importedDevDeps(pkg, sourceFiles)) {
|
|
1053
|
-
if (!(name in deps)) deps[name] = declaredDev[name];
|
|
1054
|
-
}
|
|
1055
|
-
const buildRoot = path.join(root, 'node_modules');
|
|
1056
|
-
// The project's own lockfile, materialized alongside package.json. It IS
|
|
1057
|
-
// uploaded on every deploy and was, until now, ignored — which is what made
|
|
1058
|
-
// the deployed build resolve a different version of an app dependency than
|
|
1059
|
-
// the developer's machine (tsk_f79d71ce). Missing/unreadable → empty map →
|
|
1060
|
-
// the floor-pin path, unchanged.
|
|
1061
|
-
const locked = readLockedVersions(root);
|
|
1062
|
-
const semverForLock = getSemver();
|
|
1063
|
-
const satisfiesLocked = semverForLock
|
|
1064
|
-
? (version, range) => {
|
|
1065
|
-
const wanted = (typeof range === 'string' && range.trim()) ? range.trim() : '*';
|
|
1066
|
-
if (semverForLock.validRange(wanted) === null) return false;
|
|
1067
|
-
return semverForLock.satisfies(version, wanted, { includePrerelease: true });
|
|
1068
|
-
}
|
|
1069
|
-
: null;
|
|
1070
|
-
const missing = [];
|
|
1071
|
-
// react/react-dom whose pin the baked 18 misses but the baked 19 satisfies:
|
|
1072
|
-
// collect them to symlink from the isolated react19 set instead of installing.
|
|
1073
|
-
// This is the tsk_2553662f hot-path fix — a React-19 app no longer triggers a
|
|
1074
|
-
// cold full install of react + react-dom (which dragged the whole heavy dep
|
|
1075
|
-
// tree into one over-budget job).
|
|
1076
|
-
const react19Links = [];
|
|
1077
|
-
for (const [name, range] of Object.entries(deps)) {
|
|
1078
|
-
if (TOOLCHAIN_DEPS.has(name)) continue;
|
|
1079
|
-
if (isResolvable(name, [buildRoot])) continue;
|
|
1080
|
-
if (isResolvable(name, IMAGE_NODE_MODULES) && bakedSatisfies(name, range)) continue;
|
|
1081
|
-
const r19 = react19BakedDir(name, range);
|
|
1082
|
-
if (r19) { react19Links.push({ name, dir: r19 }); continue; }
|
|
1083
|
-
// WHAT version to install is resolveInstallSpec's single decision — the
|
|
1084
|
-
// project's lockfile when it pins one inside the declared range, the
|
|
1085
|
-
// floor-pin otherwise, the declared range verbatim for first-party
|
|
1086
|
-
// @somewhere-tech/* packages. See the block comment on resolveInstallSpec.
|
|
1087
|
-
missing.push(resolveInstallSpec({
|
|
1088
|
-
name, range, lockedVersion: locked.get(name), satisfies: satisfiesLocked,
|
|
1089
|
-
}));
|
|
1090
|
-
}
|
|
1091
|
-
// Symlink the baked React-19 packages into the build root so esbuild resolves
|
|
1092
|
-
// them from root/node_modules (which it consults before the baked nodePaths) —
|
|
1093
|
-
// a single React instance per build, the baked 19, with zero install. A
|
|
1094
|
-
// symlink failure is non-fatal: drop it back into `missing` so the dep still
|
|
1095
|
-
// installs per-build (slower but correct), never a broken build.
|
|
1096
|
-
if (react19Links.length) {
|
|
1097
|
-
try { fs.mkdirSync(buildRoot, { recursive: true }); } catch { /* exists */ }
|
|
1098
|
-
for (const { name, dir } of react19Links) {
|
|
1099
|
-
const link = path.join(buildRoot, name);
|
|
1100
|
-
try {
|
|
1101
|
-
if (!fs.existsSync(link)) fs.symlinkSync(dir, link, 'dir');
|
|
1102
|
-
} catch (e) {
|
|
1103
|
-
warnings.push(`react19 link skipped for ${name}; installing per-build: ${e && e.message ? e.message : String(e)}`);
|
|
1104
|
-
missing.push(resolveInstallSpec({
|
|
1105
|
-
name, range: deps[name], lockedVersion: locked.get(name), satisfies: satisfiesLocked,
|
|
1106
|
-
}));
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
if (!missing.length) return;
|
|
1111
|
-
if (!scopedProxyReady) {
|
|
1112
|
-
throw codedError('NPM_PROXY_REQUIRED', `the scoped package proxy is required to resolve ${missing.length} dependency pin(s)`);
|
|
1113
|
-
}
|
|
1114
|
-
// Mark the build STAGE so the BUILD_TIMEOUT handler can report where it
|
|
1115
|
-
// stalled (tsk_2553662f: a timeout used to be a bare "build exceeded Nms"
|
|
1116
|
-
// with no hint whether npm install or esbuild was the culprit).
|
|
1117
|
-
if (ctx && ctx.setStage) ctx.setStage('installing');
|
|
1118
|
-
// WHICH packages to install and WHY is decided above and is the same
|
|
1119
|
-
// everywhere. HOW the install runs is the one host-specific part: the
|
|
1120
|
-
// container shells out to npm through its per-build scoped proxy with the
|
|
1121
|
-
// BUILD_TIMEOUT controller wired to the child, the CLI installs into its
|
|
1122
|
-
// own cache with no proxy. host.installPackages owns that and owns the
|
|
1123
|
-
// NPM_INSTALL_TIMEOUT / real-failure error classification with it.
|
|
1124
|
-
await host.installPackages({ root, specs: missing, ctx });
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
/**
|
|
1128
|
-
* Parse the tsconfig `paths` aliases into a list of matchers. esbuild resolves
|
|
1129
|
-
* these at build time (buildOpts.tsconfig is set), so a specifier that matches
|
|
1130
|
-
* an alias is NOT a phantom import — without this the ubiquitous `@/*` → `src/*`
|
|
1131
|
-
* alias would false-warn on every deploy that uses it (the jsxApp fixture, and
|
|
1132
|
-
* most real Vite apps). Malformed/absent tsconfig → no aliases (never a crash).
|
|
1133
|
-
* Each matcher is the key with a trailing `/*` stripped; a spec matches when it
|
|
1134
|
-
* equals the matcher OR starts with `matcher + '/'`.
|
|
1135
|
-
*/
|
|
1136
|
-
function tsconfigAliasMatchers(tsconfigText) {
|
|
1137
|
-
if (typeof tsconfigText !== 'string') return [];
|
|
1138
|
-
try {
|
|
1139
|
-
// ts.parseConfigFileTextToJson tolerates JSONC (comments/trailing commas)
|
|
1140
|
-
// that real tsconfigs carry; it's the same parser used by runReferenceCheck.
|
|
1141
|
-
const ts = imageRequire('typescript');
|
|
1142
|
-
const cfg = ts.parseConfigFileTextToJson('tsconfig.json', tsconfigText).config || {};
|
|
1143
|
-
const paths = (cfg.compilerOptions && cfg.compilerOptions.paths) || {};
|
|
1144
|
-
return Object.keys(paths).map((k) => k.replace(/\/\*$/, ''));
|
|
1145
|
-
} catch {
|
|
1146
|
-
return [];
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
/** Make the implicit project root explicit for path aliases. */
|
|
1151
|
-
function normalizeTsconfigText(tsconfigText) {
|
|
1152
|
-
if (typeof tsconfigText !== 'string') return tsconfigText;
|
|
1153
|
-
try {
|
|
1154
|
-
const parsed = imageRequire('typescript').parseConfigFileTextToJson('tsconfig.json', tsconfigText).config;
|
|
1155
|
-
const options = parsed && typeof parsed === 'object' ? parsed.compilerOptions : null;
|
|
1156
|
-
if (options && typeof options === 'object' && options.paths && options.baseUrl === undefined) {
|
|
1157
|
-
return JSON.stringify({ ...parsed, compilerOptions: { ...options, baseUrl: '.' } });
|
|
1158
|
-
}
|
|
1159
|
-
} catch { /* preserve malformed input so the compiler reports it */ }
|
|
1160
|
-
return tsconfigText;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
/**
|
|
1164
|
-
* Scan `frontendFiles` for bare imports that are NOT resolvable through any of
|
|
1165
|
-
* the mechanisms esbuild uses. Returns a Map<rootPkg, filePaths[]>.
|
|
1166
|
-
*
|
|
1167
|
-
* Called after ensureDeps so build-root node_modules reflects installed deps.
|
|
1168
|
-
* `aliasMatchers` are the tsconfig `paths` prefixes esbuild also resolves.
|
|
1169
|
-
* Wrapped in compile() so any unexpected error degrades to 0 findings, never
|
|
1170
|
-
* a crash (same discipline as reviewDependencies).
|
|
1171
|
-
*/
|
|
1172
|
-
function detectPhantomImports(frontendFiles, root, aliasMatchers = [], declaredDevDeps = []) {
|
|
1173
|
-
const buildRootNm = path.join(root, 'node_modules');
|
|
1174
|
-
const polyfillKeys = new Set(Object.keys(NODE_POLYFILLS));
|
|
1175
|
-
const devDeclared = new Set(declaredDevDeps);
|
|
1176
|
-
const isAliased = (spec) =>
|
|
1177
|
-
aliasMatchers.some((a) => spec === a || spec.startsWith(a + '/'));
|
|
1178
|
-
// Map<rootPkg, string[]> — files that import a phantom package
|
|
1179
|
-
const phantom = new Map();
|
|
1180
|
-
for (const [filePath, content] of Object.entries(frontendFiles)) {
|
|
1181
|
-
if (typeof content !== 'string') continue;
|
|
1182
|
-
// Only scan JS-family source files — CSS @import is a different resolver
|
|
1183
|
-
if (!/\.(tsx?|jsx?|mjs|cjs|js)$/i.test(filePath)) continue;
|
|
1184
|
-
// A build config is not in the browser graph at all (tsk_424174be).
|
|
1185
|
-
if (isBuildConfigFile(filePath)) continue;
|
|
1186
|
-
for (const spec of extractBareSpecifiers(content)) {
|
|
1187
|
-
// node: builtins are handled by NODE_POLYFILLS aliases; skip independently
|
|
1188
|
-
// (node:path is not a polyfillKey but is caught by the URL-shaped :// test
|
|
1189
|
-
// in extractBareSpecifiers already — this is belt-and-suspenders).
|
|
1190
|
-
if (spec.startsWith('node:')) continue;
|
|
1191
|
-
// Resolvable through a tsconfig `paths` alias (e.g. `@/App`) — esbuild
|
|
1192
|
-
// resolves these, so they are never phantom. Check the FULL specifier
|
|
1193
|
-
// (aliases like `@/App` have no npm-style root package).
|
|
1194
|
-
if (isAliased(spec)) continue;
|
|
1195
|
-
const rootPkg = specRootPkg(spec);
|
|
1196
|
-
// PROVIDED BY THE PLATFORM (`somewhere/db`, `somewhere:api`, …). Not npm
|
|
1197
|
-
// packages, so "add it to your package.json" is wrong advice on code our
|
|
1198
|
-
// own docs hand the developer (tsk_53badecfb7).
|
|
1199
|
-
if (PLATFORM_MODULE_ROOTS.has(rootPkg)) continue;
|
|
1200
|
-
// DECLARED in devDependencies. It IS declared, so "add it to your
|
|
1201
|
-
// package.json" is already done and warning about it is a false
|
|
1202
|
-
// positive with wrong remediation (tsk_424174be) — whether or not it
|
|
1203
|
-
// resolved. (Since tsk_2a7c6b33 a dev-declared package the source
|
|
1204
|
-
// imports IS installed by name, so this branch is now mostly reached
|
|
1205
|
-
// by build-config imports, which are skipped above anyway.)
|
|
1206
|
-
if (devDeclared.has(rootPkg)) continue;
|
|
1207
|
-
// Resolvable through NODE_POLYFILLS alias (e.g. 'buffer', 'process')
|
|
1208
|
-
if (polyfillKeys.has(rootPkg)) continue;
|
|
1209
|
-
// Resolvable from per-build install (declared in package.json + installed)
|
|
1210
|
-
if (isResolvable(rootPkg, [buildRootNm])) continue;
|
|
1211
|
-
// Resolvable from the pre-baked image (react@18, zustand, lucide-react, …)
|
|
1212
|
-
if (isResolvable(rootPkg, IMAGE_NODE_MODULES)) continue;
|
|
1213
|
-
// Resolvable from the isolated React-19 baked set
|
|
1214
|
-
if (REACT19_NODE_MODULES && isResolvable(rootPkg, [REACT19_NODE_MODULES])) continue;
|
|
1215
|
-
// Not resolvable through any means — phantom import
|
|
1216
|
-
if (!phantom.has(rootPkg)) phantom.set(rootPkg, []);
|
|
1217
|
-
phantom.get(rootPkg).push(filePath);
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
return phantom;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
/**
|
|
1224
|
-
* Real Tailwind + PostCSS over the customer's CSS — the headline of the
|
|
1225
|
-
* migration. v3 (`@tailwind`/config) and v4 (`@import "tailwindcss"`) run their
|
|
1226
|
-
* actual engines against the materialized filesystem, so JIT scanning sees the
|
|
1227
|
-
* source and emits the utilities the app uses. No runtime CDN, no `sw-css-noop`.
|
|
1228
|
-
* Returns an esbuild onLoad plugin scoped to `.css`.
|
|
1229
|
-
*
|
|
1230
|
-
* cwd is the build root for the duration of the build (set by caller), so
|
|
1231
|
-
* Tailwind v3 auto-loads the customer's tailwind.config.js (via jiti, ESM/TS
|
|
1232
|
-
* configs included) and resolves its `content` globs relative to the project,
|
|
1233
|
-
* exactly like `vite build` locally.
|
|
1234
|
-
*/
|
|
1235
|
-
function tailwindCssPlugin(twVersion, root) {
|
|
1236
|
-
const postcss = imageRequire('postcss');
|
|
1237
|
-
const autoprefixer = imageRequire('autoprefixer');
|
|
1238
|
-
|
|
1239
|
-
function buildProcessor() {
|
|
1240
|
-
if (twVersion === 4) {
|
|
1241
|
-
const tw4 = tw4Require('@tailwindcss/postcss');
|
|
1242
|
-
return postcss([tw4({ base: root }), autoprefixer]);
|
|
1243
|
-
}
|
|
1244
|
-
const tailwind3 = imageRequire('tailwindcss');
|
|
1245
|
-
const hasConfig = ['tailwind.config.js', 'tailwind.config.cjs', 'tailwind.config.mjs', 'tailwind.config.ts']
|
|
1246
|
-
.some((f) => fs.existsSync(path.join(root, f)));
|
|
1247
|
-
// With a config present, tailwind v3 auto-loads it from cwd (=root). Without
|
|
1248
|
-
// one (utility classes but no config file), give it explicit content globs
|
|
1249
|
-
// so JIT still scans the source instead of erroring/emitting nothing.
|
|
1250
|
-
const tw = hasConfig
|
|
1251
|
-
? tailwind3()
|
|
1252
|
-
: tailwind3({ content: [path.join(root, '**/*.{html,js,ts,jsx,tsx,mjs,cjs}')], theme: { extend: {} }, plugins: [] });
|
|
1253
|
-
return postcss([tw, autoprefixer]);
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
return {
|
|
1257
|
-
name: 'somewhere-tailwind-css',
|
|
1258
|
-
setup(build) {
|
|
1259
|
-
const processor = buildProcessor();
|
|
1260
|
-
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
|
1261
|
-
// CSS Modules (*.module.css) are class-name maps, not stylesheets to run
|
|
1262
|
-
// Tailwind over — let esbuild's native local-css loader handle them.
|
|
1263
|
-
// (Go RE2 has no lookbehind, so we filter all .css then bail here.)
|
|
1264
|
-
if (/\.module\.css$/i.test(args.path)) return undefined;
|
|
1265
|
-
let raw;
|
|
1266
|
-
try { raw = await fsp.readFile(args.path, 'utf8'); }
|
|
1267
|
-
catch (e) { return { errors: [{ text: `Cannot read CSS ${args.path}: ${e.message}` }] }; }
|
|
1268
|
-
try {
|
|
1269
|
-
const result = await processor.process(raw, { from: args.path, to: args.path });
|
|
1270
|
-
return { contents: result.css, loader: 'css' };
|
|
1271
|
-
} catch (e) {
|
|
1272
|
-
// Invariant #6: a Tailwind/PostCSS failure is a loud, actionable
|
|
1273
|
-
// compile error — never empty CSS served as "success".
|
|
1274
|
-
return { errors: [{ text: `Tailwind/PostCSS failed on ${path.relative(root, args.path)}: ${e.message}` }] };
|
|
1275
|
-
}
|
|
1276
|
-
});
|
|
1277
|
-
},
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
/**
|
|
1282
|
-
* PR5 — tsc --noEmit type checking (directive Tier 1, invariant #6). Opt-in per
|
|
1283
|
-
* request (`typecheck:true`) so the deploy hot-path stays fast; the /v1/compile
|
|
1284
|
-
* inspect-fix loop turns it on. Reports type errors in the customer's OWN typed
|
|
1285
|
-
* code. Module-resolution noise (TS2307) and implicit-any-from-untyped-deps
|
|
1286
|
-
* (TS7016) are filtered: esbuild already proved every import resolves, so a
|
|
1287
|
-
* TS2307 here only means "this dep ships no .d.ts" — not an app error. Untyped
|
|
1288
|
-
* externals collapse to `any`, so real errors in the customer's annotations
|
|
1289
|
-
* (wrong shapes, bad generics) still surface. NON-blocking: type errors are
|
|
1290
|
-
* reported as metadata, never fail the build (rule 9 — surface before gating).
|
|
1291
|
-
*/
|
|
1292
|
-
const TSC_IGNORE_CODES = new Set([2307, 7016, 2792, 6053]);
|
|
1293
|
-
function runTypecheck(root, tsconfigText) {
|
|
1294
|
-
const ts = imageRequire('typescript');
|
|
1295
|
-
const tsFiles = [];
|
|
1296
|
-
(function walk(dir) {
|
|
1297
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1298
|
-
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
1299
|
-
const p = path.join(dir, e.name);
|
|
1300
|
-
if (e.isDirectory()) walk(p);
|
|
1301
|
-
else if (/\.tsx?$/.test(e.name)) tsFiles.push(p);
|
|
1302
|
-
}
|
|
1303
|
-
})(root);
|
|
1304
|
-
if (!tsFiles.length) return { errors: [], checkedFiles: 0 };
|
|
1305
|
-
|
|
1306
|
-
let userOpts = {};
|
|
1307
|
-
if (typeof tsconfigText === 'string') {
|
|
1308
|
-
try { userOpts = (ts.parseConfigFileTextToJson('tsconfig.json', tsconfigText).config || {}).compilerOptions || {}; }
|
|
1309
|
-
catch { /* malformed tsconfig — use the lenient default below */ }
|
|
1310
|
-
}
|
|
1311
|
-
const { options } = ts.convertCompilerOptionsFromJson(
|
|
1312
|
-
{
|
|
1313
|
-
jsx: 'react-jsx', target: 'es2022', module: 'esnext', moduleResolution: 'bundler',
|
|
1314
|
-
esModuleInterop: true, skipLibCheck: true, strict: false, noImplicitAny: false,
|
|
1315
|
-
allowJs: false, ...userOpts, noEmit: true,
|
|
1316
|
-
},
|
|
1317
|
-
root,
|
|
1318
|
-
);
|
|
1319
|
-
const program = ts.createProgram(tsFiles, options);
|
|
1320
|
-
const errors = [];
|
|
1321
|
-
for (const d of ts.getPreEmitDiagnostics(program)) {
|
|
1322
|
-
if (d.category !== ts.DiagnosticCategory.Error) continue;
|
|
1323
|
-
if (d.code && TSC_IGNORE_CODES.has(d.code)) continue;
|
|
1324
|
-
const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
|
|
1325
|
-
let loc = {};
|
|
1326
|
-
if (d.file && d.start != null) {
|
|
1327
|
-
const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
|
|
1328
|
-
loc = { file: path.relative(root, d.file.fileName), line: line + 1, col: character + 1 };
|
|
1329
|
-
}
|
|
1330
|
-
errors.push({ code: `TS${d.code}`, message, ...loc });
|
|
1331
|
-
}
|
|
1332
|
-
return { errors, checkedFiles: tsFiles.length };
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
/**
|
|
1336
|
-
* Deploy-validation reference check (tsk §1 deploy-validation-gate). The
|
|
1337
|
-
* narrow, ALWAYS-SAFE cousin of runTypecheck, purpose-built for the deploy
|
|
1338
|
-
* gate that flags an UNDEFINED REFERENCE — the Bolo `sanitizeForSpeech is
|
|
1339
|
-
* not defined` class: code esbuild bundles past (valid syntax) but that
|
|
1340
|
-
* throws at runtime because nothing in the graph defines the name.
|
|
1341
|
-
*
|
|
1342
|
-
* Differences from runTypecheck that make it catch that class without
|
|
1343
|
-
* false-positiving on normal customer code:
|
|
1344
|
-
* - Walks .js/.jsx TOO (allowJs:true + checkJs:true). The Bolo bug was in
|
|
1345
|
-
* a .jsx; runTypecheck's allowJs:false skips those files entirely.
|
|
1346
|
-
* - Includes the DOM lib so window/fetch/localStorage/navigator/etc.
|
|
1347
|
-
* resolve — without it every browser global reads as an undefined name
|
|
1348
|
-
* (a wall of false positives on real frontend code).
|
|
1349
|
-
* - Filters module-resolution noise (TS2307 etc., same set as
|
|
1350
|
-
* runTypecheck) AND scopes the SURFACED set to reference codes only on
|
|
1351
|
-
* the worker side — here we return EVERY non-ignored error and let the
|
|
1352
|
-
* worker pick the reference subset, so the container stays the single
|
|
1353
|
-
* source of "what tsc said" and the gate's policy lives in one place.
|
|
1354
|
-
*
|
|
1355
|
-
* Returns the same {code, message, file, line, col} shape as runTypecheck.
|
|
1356
|
-
* Never throws into compile() — the caller wraps it.
|
|
1357
|
-
*/
|
|
1358
|
-
function runReferenceCheck(root, tsconfigText) {
|
|
1359
|
-
const ts = imageRequire('typescript');
|
|
1360
|
-
const checkFiles = [];
|
|
1361
|
-
(function walk(dir) {
|
|
1362
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1363
|
-
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
|
|
1364
|
-
const p = path.join(dir, e.name);
|
|
1365
|
-
if (e.isDirectory()) walk(p);
|
|
1366
|
-
else if (/\.(?:tsx?|jsx?|mjs|cjs)$/.test(e.name)) checkFiles.push(p);
|
|
1367
|
-
}
|
|
1368
|
-
})(root);
|
|
1369
|
-
if (!checkFiles.length) return { errors: [], checkedFiles: 0 };
|
|
1370
|
-
|
|
1371
|
-
let userOpts = {};
|
|
1372
|
-
if (typeof tsconfigText === 'string') {
|
|
1373
|
-
try { userOpts = (ts.parseConfigFileTextToJson('tsconfig.json', tsconfigText).config || {}).compilerOptions || {}; }
|
|
1374
|
-
catch { /* malformed tsconfig — use the lenient default below */ }
|
|
1375
|
-
}
|
|
1376
|
-
const { options } = ts.convertCompilerOptionsFromJson(
|
|
1377
|
-
{
|
|
1378
|
-
jsx: 'react-jsx', target: 'es2022', module: 'esnext', moduleResolution: 'bundler',
|
|
1379
|
-
esModuleInterop: true, skipLibCheck: true, strict: false, noImplicitAny: false,
|
|
1380
|
-
// DOM + es2022 so browser globals (window/fetch/localStorage/…) resolve
|
|
1381
|
-
// and don't read as undefined names — the whole reason this differs from
|
|
1382
|
-
// runTypecheck. NOTE: convertCompilerOptionsFromJson expects the SHORT
|
|
1383
|
-
// lib names ('es2022','dom'), not the lib.*.d.ts filenames (those are
|
|
1384
|
-
// rejected → empty lib → every browser global false-positives).
|
|
1385
|
-
lib: ['es2022', 'dom', 'dom.iterable'],
|
|
1386
|
-
...userOpts,
|
|
1387
|
-
// These are forced ON regardless of the customer's tsconfig — the gate
|
|
1388
|
-
// exists to check .js/.jsx for undefined refs, and a customer
|
|
1389
|
-
// allowJs:false would silently disable it.
|
|
1390
|
-
allowJs: true, checkJs: true, noEmit: true,
|
|
1391
|
-
},
|
|
1392
|
-
root,
|
|
1393
|
-
);
|
|
1394
|
-
const program = ts.createProgram(checkFiles, options);
|
|
1395
|
-
const errors = [];
|
|
1396
|
-
for (const d of ts.getPreEmitDiagnostics(program)) {
|
|
1397
|
-
if (d.category !== ts.DiagnosticCategory.Error) continue;
|
|
1398
|
-
if (d.code && TSC_IGNORE_CODES.has(d.code)) continue;
|
|
1399
|
-
const message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
|
|
1400
|
-
let loc = {};
|
|
1401
|
-
if (d.file && d.start != null) {
|
|
1402
|
-
const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
|
|
1403
|
-
loc = { file: path.relative(root, d.file.fileName), line: line + 1, col: character + 1 };
|
|
1404
|
-
}
|
|
1405
|
-
errors.push({ code: `TS${d.code}`, message, ...loc });
|
|
1406
|
-
}
|
|
1407
|
-
return { errors, checkedFiles: checkFiles.length };
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
async function compileTransforms(body) {
|
|
1411
|
-
const entries = Array.isArray(body.entries) ? body.entries : [];
|
|
1412
|
-
if (!entries.length) throw new Error('`entries` is required');
|
|
1413
|
-
const transforms = {};
|
|
1414
|
-
for (const entry of entries) {
|
|
1415
|
-
assertSourcePath(entry);
|
|
1416
|
-
const source = body.files[entry];
|
|
1417
|
-
const loader = sourceLoader(entry);
|
|
1418
|
-
if (typeof source !== 'string' || (loader !== 'jsx' && loader !== 'tsx')) {
|
|
1419
|
-
throw new Error(`transform entry ${JSON.stringify(entry)} must be a .jsx or .tsx source file`);
|
|
1420
|
-
}
|
|
1421
|
-
const out = await esbuild.transform(source, {
|
|
1422
|
-
loader,
|
|
1423
|
-
format: 'esm',
|
|
1424
|
-
target: 'es2022',
|
|
1425
|
-
jsx: 'automatic',
|
|
1426
|
-
sourcemap: 'inline',
|
|
1427
|
-
sourcefile: entry,
|
|
1428
|
-
logLevel: 'silent',
|
|
1429
|
-
});
|
|
1430
|
-
transforms[entry] = { js: out.code, warnings: out.warnings.map((warning) => warning.text) };
|
|
1431
|
-
}
|
|
1432
|
-
return { ok: true, transforms, warnings: [], compiler: compilerStamp() };
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
async function parseSources(body) {
|
|
1436
|
-
const errors = [];
|
|
1437
|
-
for (const [file, source] of Object.entries(body.files)) {
|
|
1438
|
-
if (typeof source !== 'string') continue;
|
|
1439
|
-
const loader = sourceLoader(file);
|
|
1440
|
-
if (!loader || loader === 'json') continue;
|
|
1441
|
-
try {
|
|
1442
|
-
await esbuild.transform(source, { loader, target: 'esnext', format: 'esm', logLevel: 'silent', sourcefile: file });
|
|
1443
|
-
} catch (err) {
|
|
1444
|
-
const messages = Array.isArray(err && err.errors) ? err.errors : [];
|
|
1445
|
-
if (messages.length) {
|
|
1446
|
-
for (const message of messages.slice(0, 5)) {
|
|
1447
|
-
errors.push({
|
|
1448
|
-
file,
|
|
1449
|
-
line: message.location && message.location.line,
|
|
1450
|
-
column: message.location ? message.location.column + 1 : undefined,
|
|
1451
|
-
message: message.text,
|
|
1452
|
-
line_text: message.location && message.location.lineText,
|
|
1453
|
-
});
|
|
1454
|
-
}
|
|
1455
|
-
} else {
|
|
1456
|
-
errors.push({ file, message: err && err.message ? err.message : String(err) });
|
|
1457
|
-
}
|
|
1458
|
-
if (errors.length >= 20) break;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
return { ok: true, errors, warnings: [], compiler: compilerStamp() };
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
async function compileFunctionEntries(root, entries, tsconfigText) {
|
|
1465
|
-
if (!entries.length) return { functions: {}, warnings: [], metafile: null, graph: { edges: [] } };
|
|
1466
|
-
const entryPoints = {};
|
|
1467
|
-
const outputNames = new Map();
|
|
1468
|
-
entries.forEach((entry, index) => {
|
|
1469
|
-
const name = `__sw_function_${String(index).padStart(4, '0')}`;
|
|
1470
|
-
entryPoints[name] = entry;
|
|
1471
|
-
outputNames.set(`${name}.js`, entry);
|
|
1472
|
-
});
|
|
1473
|
-
const buildOptions = {
|
|
1474
|
-
absWorkingDir: root,
|
|
1475
|
-
entryPoints,
|
|
1476
|
-
bundle: true,
|
|
1477
|
-
splitting: false,
|
|
1478
|
-
format: 'esm',
|
|
1479
|
-
platform: 'neutral',
|
|
1480
|
-
target: 'es2022',
|
|
1481
|
-
jsx: 'automatic',
|
|
1482
|
-
sourcemap: 'inline',
|
|
1483
|
-
write: false,
|
|
1484
|
-
metafile: true,
|
|
1485
|
-
outdir: 'function-out',
|
|
1486
|
-
entryNames: '[name]',
|
|
1487
|
-
nodePaths: IMAGE_NODE_MODULES,
|
|
1488
|
-
external: ['node:*', 'cloudflare:*', 'workerd:*'],
|
|
1489
|
-
define: {
|
|
1490
|
-
'import.meta.env.DEV': 'false',
|
|
1491
|
-
'import.meta.env.PROD': 'true',
|
|
1492
|
-
'import.meta.env.MODE': '"production"',
|
|
1493
|
-
'import.meta.env.SSR': 'true',
|
|
1494
|
-
'import.meta.env.BASE_URL': '"/"',
|
|
1495
|
-
'process.env.NODE_ENV': '"production"',
|
|
1496
|
-
},
|
|
1497
|
-
plugins: [functionLocalImportPlugin(root)],
|
|
1498
|
-
};
|
|
1499
|
-
const tsconfigPath = path.join(root, 'tsconfig.json');
|
|
1500
|
-
if (fs.existsSync(tsconfigPath)) buildOptions.tsconfig = tsconfigPath;
|
|
1501
|
-
const result = await esbuild.build(buildOptions);
|
|
1502
|
-
const completeGraph = graphFromMetafile(result.metafile, root);
|
|
1503
|
-
const functions = {};
|
|
1504
|
-
for (const output of result.outputFiles || []) {
|
|
1505
|
-
const entry = outputNames.get(path.basename(output.path));
|
|
1506
|
-
if (!entry) continue;
|
|
1507
|
-
functions[entry] = {
|
|
1508
|
-
js: output.text,
|
|
1509
|
-
inlined: [entry],
|
|
1510
|
-
warnings: [],
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
for (const entry of entries) {
|
|
1514
|
-
if (!functions[entry]) throw new Error(`bundler emitted no output for ${entry}`);
|
|
1515
|
-
}
|
|
1516
|
-
return { functions, warnings: result.warnings.map((warning) => warning.text), metafile: result.metafile, graph: completeGraph };
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
async function compile(body, ctx) {
|
|
1520
|
-
validateCompileInput(body);
|
|
1521
|
-
const operation = body.operation || 'project';
|
|
1522
|
-
if (operation === 'parse') return parseSources(body);
|
|
1523
|
-
if (operation !== 'project') throw new Error(`unsupported compile operation: ${operation}`);
|
|
1524
|
-
const result = await compileProject(body, ctx);
|
|
1525
|
-
const stamped = { ...result, compiler: compilerStamp(result.source_digest) };
|
|
1526
|
-
enforceInlineResultCap(body, stamped);
|
|
1527
|
-
return stamped;
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
// ctx (tsk_2553662f) carries the BUILD_TIMEOUT plumbing: `signal` (the
|
|
1531
|
-
// AbortController the handler aborts on timeout), `setStage(name)` so the
|
|
1532
|
-
// timeout 504 can report which stage stalled, and `npmChild` (the in-flight npm
|
|
1533
|
-
// process, so the abort kills it). Optional — the in-process harness/test path
|
|
1534
|
-
// calls compile(body) with no ctx and just runs to completion.
|
|
1535
|
-
async function compileProject(body, ctx) {
|
|
1536
|
-
const files = { ...body.files };
|
|
1537
|
-
const requestedEntry = typeof body.entry === 'string' && body.entry ? body.entry : null;
|
|
1538
|
-
const entry = requestedEntry || '__somewhere_empty_entry.mjs';
|
|
1539
|
-
if (!files || typeof files !== 'object') throw new Error('`files` map is required');
|
|
1540
|
-
if (requestedEntry && files[entry] === undefined) throw new Error(`entry "${entry}" not in files map`);
|
|
1541
|
-
if (!requestedEntry) files[entry] = 'export default null;\n';
|
|
1542
|
-
const functionEntries = [...new Set(body.function_entries || [])].sort();
|
|
1543
|
-
const functionPaths = new Set(body.function_paths || functionEntries);
|
|
1544
|
-
const transformEntries = [...new Set(body.transform_entries || [])]
|
|
1545
|
-
.filter((item) => item !== requestedEntry)
|
|
1546
|
-
.sort();
|
|
1547
|
-
for (const functionEntry of functionEntries) {
|
|
1548
|
-
if (typeof files[functionEntry] !== 'string') throw new Error(`function entry ${JSON.stringify(functionEntry)} not in files map`);
|
|
1549
|
-
}
|
|
1550
|
-
for (const transformEntry of transformEntries) {
|
|
1551
|
-
if (typeof files[transformEntry] !== 'string') throw new Error(`transform entry ${JSON.stringify(transformEntry)} not in files map`);
|
|
1552
|
-
}
|
|
1553
|
-
const digest = sourceDigest(body);
|
|
1554
|
-
const parsed = await parseSources({ files: body.files });
|
|
1555
|
-
if (parsed.errors.length) {
|
|
1556
|
-
const err = codedError('SOURCE_PARSE_ERROR', parsed.errors.map((error) => `${error.file}: ${error.message}`).join('; '));
|
|
1557
|
-
// The joined message keeps its exact existing text (the worker's error
|
|
1558
|
-
// copy reads it), but the STRUCTURED errors ride along: {file, line,
|
|
1559
|
-
// column, message, line_text} per bad file. A caller that wants to point
|
|
1560
|
-
// at the offending line — the local dev loop's terminal and its in-page
|
|
1561
|
-
// overlay — should not have to regex a paragraph back apart.
|
|
1562
|
-
err.source_errors = parsed.errors;
|
|
1563
|
-
throw err;
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// Classify functions out of the frontend graph. Every routed entry is still
|
|
1567
|
-
// compiled below in THIS transaction, from the same materialized root and
|
|
1568
|
-
// resolved dependency tree.
|
|
1569
|
-
const functionRoutes = [];
|
|
1570
|
-
const frontendFiles = {};
|
|
1571
|
-
for (const p of Object.keys(files)) {
|
|
1572
|
-
if (functionPaths.has(p) || isFunctionPath(p)) functionRoutes.push(p);
|
|
1573
|
-
else frontendFiles[p] = files[p];
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
const packageJsonText = typeof body.package_json === 'string'
|
|
1577
|
-
? body.package_json
|
|
1578
|
-
: files['package.json'];
|
|
1579
|
-
const tsconfigText = normalizeTsconfigText(
|
|
1580
|
-
typeof body.tsconfig === 'string' ? body.tsconfig : files['tsconfig.json'],
|
|
1581
|
-
);
|
|
1582
|
-
let pkg = {};
|
|
1583
|
-
if (typeof packageJsonText === 'string') {
|
|
1584
|
-
try { pkg = JSON.parse(packageJsonText); } catch { /* malformed → no extra deps */ }
|
|
1585
|
-
}
|
|
1586
|
-
const viteEnv = body.vite_env || {};
|
|
1587
|
-
const twVersion = detectTailwind(files);
|
|
1588
|
-
const warnings = [];
|
|
1589
|
-
|
|
1590
|
-
// Per-phase wall-clock instrumentation (tsk_241c7c9f). Date.now() is fine
|
|
1591
|
-
// here — real Node in a container. Each value is ms for that phase; null =
|
|
1592
|
-
// phase did not run on this path. Echoed under metadata.timing_ms, purely
|
|
1593
|
-
// additive (no existing metadata field touched).
|
|
1594
|
-
const _t0 = Date.now();
|
|
1595
|
-
const timing = { materialize: null, install: null, typed_functions: null, bundle: null, refcheck: null, upload: null, total: null };
|
|
1596
|
-
const _ms = (start) => Date.now() - start;
|
|
1597
|
-
|
|
1598
|
-
const _tMat = Date.now();
|
|
1599
|
-
const root = await materialize(files, body.binary_files);
|
|
1600
|
-
if (typeof packageJsonText === 'string') {
|
|
1601
|
-
await fsp.writeFile(path.join(root, 'package.json'), packageJsonText, 'utf8').catch(() => {});
|
|
1602
|
-
}
|
|
1603
|
-
if (typeof tsconfigText === 'string') {
|
|
1604
|
-
await fsp.writeFile(path.join(root, 'tsconfig.json'), tsconfigText, 'utf8').catch(() => {});
|
|
1605
|
-
}
|
|
1606
|
-
timing.materialize = _ms(_tMat);
|
|
1607
|
-
|
|
1608
|
-
// ── validate_only fast path (tsk §1 deploy-validation-gate) ────────────
|
|
1609
|
-
// The deploy validation gate asks ONLY "does the module graph have an
|
|
1610
|
-
// undefined reference?" — it does NOT want a bundle, Tailwind, or npm
|
|
1611
|
-
// install (the slow parts). Run just the reference check over the
|
|
1612
|
-
// materialized graph and return metadata.validation_errors. No esbuild,
|
|
1613
|
-
// no chdir, no chunk emit. Wrapped so a tsc crash degrades to "no
|
|
1614
|
-
// validation errors" (ran-but-empty is indistinguishable from a clean
|
|
1615
|
-
// graph here, which is the safe direction — the worker treats an absent
|
|
1616
|
-
// validation_errors array as "not reference-checked", never as "clean").
|
|
1617
|
-
if (body.validate_only) {
|
|
1618
|
-
let validationErrors = null;
|
|
1619
|
-
const _tRef = Date.now();
|
|
1620
|
-
try {
|
|
1621
|
-
validationErrors = runReferenceCheck(root, tsconfigText).errors;
|
|
1622
|
-
} catch (e) {
|
|
1623
|
-
warnings.push(`reference check skipped: ${e && e.message ? e.message : String(e)}`);
|
|
1624
|
-
}
|
|
1625
|
-
timing.refcheck = _ms(_tRef);
|
|
1626
|
-
await fsp.rm(root, { recursive: true, force: true }).catch(() => {});
|
|
1627
|
-
timing.total = _ms(_t0);
|
|
1628
|
-
return {
|
|
1629
|
-
ok: true,
|
|
1630
|
-
validate_only: true,
|
|
1631
|
-
warnings,
|
|
1632
|
-
metadata: { validation_errors: validationErrors, timing_ms: timing },
|
|
1633
|
-
};
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
const prevCwd = process.cwd();
|
|
1637
|
-
try {
|
|
1638
|
-
// PR2 npm proxy — when the deploy path supplies a registry+token, route this
|
|
1639
|
-
// build's `npm install` through the worker /v1/npm cache. Written as a
|
|
1640
|
-
// build-root .npmrc; ensureDeps runs npm with cwd=root so it's picked up.
|
|
1641
|
-
// always-auth so the bearer rides tarball GETs too. The token is a
|
|
1642
|
-
// short-lived proxy capability, NOT a registry credential (credential desert
|
|
1643
|
-
// preserved). Without it, a build that needs an install fails closed.
|
|
1644
|
-
const hasScopedProxy = typeof body.npm_registry === 'string' && !!body.npm_registry
|
|
1645
|
-
&& typeof body.npm_token === 'string' && !!body.npm_token;
|
|
1646
|
-
if (hasScopedProxy) {
|
|
1647
|
-
try {
|
|
1648
|
-
const reg = body.npm_registry.endsWith('/') ? body.npm_registry : body.npm_registry + '/';
|
|
1649
|
-
const authKey = reg.replace(/^https?:/, '');
|
|
1650
|
-
await fsp.writeFile(
|
|
1651
|
-
path.join(root, '.npmrc'),
|
|
1652
|
-
`registry=${reg}\n${authKey}:_authToken=${body.npm_token}\nalways-auth=true\n`,
|
|
1653
|
-
'utf8',
|
|
1654
|
-
);
|
|
1655
|
-
} catch (e) {
|
|
1656
|
-
warnings.push(`npm proxy config skipped: ${e && e.message ? e.message : String(e)}`);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
// Resolve the long-tail deps before building (invariant #2). Awaited +
|
|
1661
|
-
// abortable now (tsk_2553662f): a heavy install can be killed by the
|
|
1662
|
-
// BUILD_TIMEOUT instead of blocking the event loop past the budget.
|
|
1663
|
-
const _tInstall = Date.now();
|
|
1664
|
-
// Two different questions. `hasScopedProxy` is "did this REQUEST carry
|
|
1665
|
-
// proxy credentials", and it decides the .npmrc above. Whether an
|
|
1666
|
-
// install may run AT ALL without them is a property of the HOST: the
|
|
1667
|
-
// container is egress-locked and must fail closed, the CLI reaches npm
|
|
1668
|
-
// directly through the developer's own config. Default is the container's
|
|
1669
|
-
// refusal, so opening it is always deliberate.
|
|
1670
|
-
// `files` (frontend AND function sources) is what decides the one
|
|
1671
|
-
// devDependency carve-out — a dev-declared package the app imports is
|
|
1672
|
-
// installed by name so the install can otherwise skip the dev closure.
|
|
1673
|
-
await ensureDeps(root, pkg, warnings, ctx, hasScopedProxy || host.requiresPackageProxy === false, files);
|
|
1674
|
-
timing.install = _ms(_tInstall);
|
|
1675
|
-
|
|
1676
|
-
// Dependency review (PR1) — WARN-FIRST, non-blocking. A scanner bug must
|
|
1677
|
-
// never break a build (that's the exact rule-9 anti-pattern), so it's
|
|
1678
|
-
// wrapped: on error we say so in warnings and continue without the block.
|
|
1679
|
-
let dependencyReview = null;
|
|
1680
|
-
try {
|
|
1681
|
-
dependencyReview = reviewDependencies(root, pkg);
|
|
1682
|
-
if (dependencyReview.knownBad.length) {
|
|
1683
|
-
const list = dependencyReview.knownBad.map((b) => `${b.name}@${b.version}`).join(', ');
|
|
1684
|
-
const n = dependencyReview.knownBad.length;
|
|
1685
|
-
warnings.push(
|
|
1686
|
-
`Security: ${n} ${n === 1 ? 'dependency resolves' : 'dependencies resolve'} to a release flagged as compromised in a known npm supply-chain incident: ${list}. Your build completed — this is a warning, not a block — but we strongly recommend pinning a safe version.`,
|
|
1687
|
-
);
|
|
1688
|
-
}
|
|
1689
|
-
} catch (e) {
|
|
1690
|
-
warnings.push(`dependency review skipped: ${e && e.message ? e.message : String(e)}`);
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
// Tailwind v3 config auto-load + content scanning resolve relative to cwd.
|
|
1694
|
-
process.chdir(root);
|
|
1695
|
-
|
|
1696
|
-
// v4's `@import "tailwindcss"` is resolved (by @tailwindcss/postcss's
|
|
1697
|
-
// resolver) relative to the importing CSS file — it expects the project to
|
|
1698
|
-
// have `tailwindcss` installed. We substitute the isolated v4 engine, so
|
|
1699
|
-
// link it into the build root's node_modules where the resolver walks up
|
|
1700
|
-
// to find it. (No-op if the customer somehow shipped their own.)
|
|
1701
|
-
if (twVersion === 4) {
|
|
1702
|
-
const nm = path.join(root, 'node_modules');
|
|
1703
|
-
await fsp.mkdir(nm, { recursive: true });
|
|
1704
|
-
const link = path.join(nm, 'tailwindcss');
|
|
1705
|
-
if (!fs.existsSync(link)) {
|
|
1706
|
-
if (host.tw4TailwindDir) await fsp.symlink(host.tw4TailwindDir, link, 'dir').catch(() => {});
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
// Typed server functions are sentinel-gated. analyzeTypedFunctions returns
|
|
1711
|
-
// after one string scan when no exact `somewhere:v1` candidate exists, so
|
|
1712
|
-
// ordinary deploys never construct a TypeScript Program. Stage 1 is
|
|
1713
|
-
// warning-only: an analyzer defect must not reject otherwise-working code.
|
|
1714
|
-
let typedAnalysis = {
|
|
1715
|
-
manifest: null,
|
|
1716
|
-
summary: { procedures: 0, contract_digest: null, warnings: 0 },
|
|
1717
|
-
warnings: [],
|
|
1718
|
-
declaration: null,
|
|
1719
|
-
runtime: typedFunctions.RUNTIME_SOURCE,
|
|
1720
|
-
timing_ms: { scan: 0, typecheck: 0, total: 0 },
|
|
1721
|
-
};
|
|
1722
|
-
try {
|
|
1723
|
-
typedAnalysis = typedFunctions.analyzeTypedFunctions({
|
|
1724
|
-
loadTypescript: () => imageRequire('typescript'),
|
|
1725
|
-
root,
|
|
1726
|
-
files,
|
|
1727
|
-
functionEntries,
|
|
1728
|
-
tsconfigText,
|
|
1729
|
-
});
|
|
1730
|
-
warnings.push(...typedAnalysis.warnings);
|
|
1731
|
-
} catch (e) {
|
|
1732
|
-
warnings.push(`Typed function check skipped; this deploy continued unchanged. Fix: retry the deploy, then contact support if the warning repeats. (${e && e.message ? e.message : String(e)})`);
|
|
1733
|
-
}
|
|
1734
|
-
timing.typed_functions = typedAnalysis.timing_ms;
|
|
1735
|
-
|
|
1736
|
-
const tsconfigPath = path.join(root, 'tsconfig.json');
|
|
1737
|
-
const buildOpts = {
|
|
1738
|
-
absWorkingDir: root,
|
|
1739
|
-
entryPoints: [entry],
|
|
1740
|
-
bundle: true,
|
|
1741
|
-
splitting: true,
|
|
1742
|
-
format: 'esm',
|
|
1743
|
-
target: 'es2022',
|
|
1744
|
-
jsx: 'automatic',
|
|
1745
|
-
sourcemap: 'inline',
|
|
1746
|
-
write: false,
|
|
1747
|
-
metafile: true,
|
|
1748
|
-
outdir: 'out',
|
|
1749
|
-
entryNames: '[name]-[hash]',
|
|
1750
|
-
chunkNames: 'chunk-[hash]',
|
|
1751
|
-
assetNames: 'asset-[hash]',
|
|
1752
|
-
// A .js entry only ever comes from the CRA adapter (module-script
|
|
1753
|
-
// detection is .jsx/.tsx-only, so no pre-existing project can reach
|
|
1754
|
-
// here with one) — CRA allows JSX in .js files, so parse all .js as
|
|
1755
|
-
// JSX for those builds. The jsx loader is a superset of js.
|
|
1756
|
-
loader: { ...ASSET_LOADERS, ...(/\.js$/i.test(entry) ? { '.js': 'jsx' } : {}) },
|
|
1757
|
-
// esbuild resolves bare deps from root/node_modules first, then the
|
|
1758
|
-
// pre-baked image set (flag #1). Single node_modules tree per build =
|
|
1759
|
-
// single React instance, without the retired CDN dependency workaround.
|
|
1760
|
-
nodePaths: IMAGE_NODE_MODULES,
|
|
1761
|
-
// Node builtins → browser polyfills (so packages importing buffer/stream/
|
|
1762
|
-
// etc. resolve instead of hard-erroring). esbuild resolves these targets
|
|
1763
|
-
// from the pre-baked node_modules via nodePaths.
|
|
1764
|
-
alias: { ...NODE_POLYFILLS },
|
|
1765
|
-
define: {
|
|
1766
|
-
'import.meta.env.DEV': 'false',
|
|
1767
|
-
'import.meta.env.PROD': 'true',
|
|
1768
|
-
'import.meta.env.MODE': '"production"',
|
|
1769
|
-
'import.meta.env.SSR': 'false',
|
|
1770
|
-
'import.meta.env.BASE_URL': '"/"',
|
|
1771
|
-
'process.env.NODE_ENV': '"production"',
|
|
1772
|
-
// Node polyfills (the buffer/process packages) reference the `global`
|
|
1773
|
-
// identifier, which the browser doesn't define. Map it to globalThis.
|
|
1774
|
-
global: 'globalThis',
|
|
1775
|
-
...buildViteEnvDefines(viteEnv, files),
|
|
1776
|
-
...buildReactEnvDefines(viteEnv, files),
|
|
1777
|
-
},
|
|
1778
|
-
plugins: [
|
|
1779
|
-
typedFunctions.virtualApiPlugin(esbuild),
|
|
1780
|
-
urlAssetPlugin(root, body.binary_paths),
|
|
1781
|
-
svgPlugin(root),
|
|
1782
|
-
...(twVersion > 0 ? [tailwindCssPlugin(twVersion, root)] : []),
|
|
1783
|
-
],
|
|
1784
|
-
};
|
|
1785
|
-
if (fs.existsSync(tsconfigPath)) buildOpts.tsconfig = tsconfigPath;
|
|
1786
|
-
|
|
1787
|
-
// Stage marker (tsk_2553662f): we're past install, now bundling. If the
|
|
1788
|
-
// BUILD_TIMEOUT fires during esbuild, its 504 reports stage='bundling'.
|
|
1789
|
-
if (ctx && ctx.setStage) ctx.setStage('bundling');
|
|
1790
|
-
// Use an esbuild CONTEXT so a BUILD_TIMEOUT abort can CANCEL the in-flight
|
|
1791
|
-
// build (and dispose the worker) instead of leaking it. Without a signal
|
|
1792
|
-
// (the local harness) this is a plain build()+dispose, behaviourally
|
|
1793
|
-
// identical to the old esbuild.build() call.
|
|
1794
|
-
let result;
|
|
1795
|
-
const _tBundle = Date.now();
|
|
1796
|
-
const bctx = await esbuild.context(buildOpts);
|
|
1797
|
-
try {
|
|
1798
|
-
const onAbort = () => { bctx.cancel().catch(() => {}); };
|
|
1799
|
-
if (ctx && ctx.signal) {
|
|
1800
|
-
if (ctx.signal.aborted) onAbort();
|
|
1801
|
-
else ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
1802
|
-
}
|
|
1803
|
-
result = await bctx.rebuild();
|
|
1804
|
-
if (ctx && ctx.signal) ctx.signal.removeEventListener('abort', onAbort);
|
|
1805
|
-
} finally {
|
|
1806
|
-
await bctx.dispose();
|
|
1807
|
-
timing.bundle = _ms(_tBundle);
|
|
1808
|
-
}
|
|
1809
|
-
for (const w of result.warnings) warnings.push(w.text);
|
|
1810
|
-
|
|
1811
|
-
// Phantom-import detection (tsk_8e460b69): esbuild tree-shakes unused named
|
|
1812
|
-
// imports before resolution, so `import { x } from 'missing-pkg'` where `x`
|
|
1813
|
-
// is never referenced silently succeeds. Scan the source for bare specifiers
|
|
1814
|
-
// that are NOT resolvable through any of esbuild's search paths and surface
|
|
1815
|
-
// them as warnings. RULE 9 — always non-blocking; scanner errors degrade to
|
|
1816
|
-
// 0 findings, never a crash.
|
|
1817
|
-
try {
|
|
1818
|
-
const aliasMatchers = tsconfigAliasMatchers(tsconfigText);
|
|
1819
|
-
const phantom = detectPhantomImports(
|
|
1820
|
-
frontendFiles, root, aliasMatchers, Object.keys((pkg && pkg.devDependencies) || {}),
|
|
1821
|
-
);
|
|
1822
|
-
for (const [pkg, files] of phantom) {
|
|
1823
|
-
const uniq = [...new Set(files)];
|
|
1824
|
-
const where = uniq.slice(0, 2).join(', ') + (uniq.length > 2 ? ` and ${uniq.length - 2} other file(s)` : '');
|
|
1825
|
-
warnings.push(
|
|
1826
|
-
`\`${pkg}\` is imported in ${where} but is not in your package.json and could not be resolved — if that code path runs, your app will fail to load it. Add it to the \`dependencies\` in your package.json.`,
|
|
1827
|
-
);
|
|
1828
|
-
}
|
|
1829
|
-
} catch (e) {
|
|
1830
|
-
warnings.push(`phantom-import scan skipped: ${e && e.message ? e.message : String(e)}`);
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
const entryBase = entry.split('/').pop().replace(/\.[^.]+$/, '');
|
|
1834
|
-
const chunks = {};
|
|
1835
|
-
let entryChunk = null;
|
|
1836
|
-
for (const f of result.outputFiles || []) {
|
|
1837
|
-
const name = f.path.split('/').pop();
|
|
1838
|
-
if (requestedEntry) {
|
|
1839
|
-
chunks[name] = f.text;
|
|
1840
|
-
if (!entryChunk && name.startsWith(`${entryBase}-`) && name.endsWith('.js')) entryChunk = name;
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
if (requestedEntry && !entryChunk) throw new Error('bundler emitted no entry chunk');
|
|
1844
|
-
|
|
1845
|
-
// Every routed function is compiled in one batched esbuild invocation from
|
|
1846
|
-
// this same root/dependency tree. No per-entry container request exists.
|
|
1847
|
-
if (ctx && ctx.setStage) ctx.setStage('bundling-functions');
|
|
1848
|
-
const functionBuild = await compileFunctionEntries(root, functionEntries, tsconfigText);
|
|
1849
|
-
warnings.push(...functionBuild.warnings);
|
|
1850
|
-
|
|
1851
|
-
// Independently referenced JSX/TSX module entries are transformation
|
|
1852
|
-
// artifacts of this same complete project transaction, not another
|
|
1853
|
-
// publication request.
|
|
1854
|
-
const transformBuild = transformEntries.length
|
|
1855
|
-
? await compileTransforms({ entries: transformEntries, files })
|
|
1856
|
-
: { transforms: {} };
|
|
1857
|
-
const transforms = transformBuild.transforms || {};
|
|
1858
|
-
|
|
1859
|
-
// PR5 — opt-in type checking. Non-blocking: a type error is reported in
|
|
1860
|
-
// metadata, never a build failure (rule 9). A tsc crash degrades to a
|
|
1861
|
-
// warning, not a lost build.
|
|
1862
|
-
let typeErrors = null;
|
|
1863
|
-
if (body.typecheck) {
|
|
1864
|
-
try { typeErrors = runTypecheck(root, tsconfigText).errors; }
|
|
1865
|
-
catch (e) { warnings.push(`type check skipped: ${e && e.message ? e.message : String(e)}`); }
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
// Manifest path (tsk_118c1ecc): when the worker handed us an upload
|
|
1869
|
-
// capability, write the chunks to storage OURSELVES and answer with a
|
|
1870
|
-
// manifest only — the result stays small no matter how heavy the build.
|
|
1871
|
-
// Otherwise enforce the worker's inline cap: a result the deploy Worker
|
|
1872
|
-
// provably cannot parse must be a LOUD deterministic error here, never a
|
|
1873
|
-
// 36MB body that hangs the request.
|
|
1874
|
-
let uploadedChunks = null;
|
|
1875
|
-
const functionArtifacts = Object.entries(functionBuild.functions).map(([pathname, artifact]) =>
|
|
1876
|
-
artifactDescriptor(pathname, 'function', artifact.js));
|
|
1877
|
-
const transformArtifacts = Object.entries(transforms).map(([pathname, artifact]) =>
|
|
1878
|
-
artifactDescriptor(pathname, 'transform', artifact.js));
|
|
1879
|
-
const staticArtifacts = Object.entries(chunks).map(([name, text]) =>
|
|
1880
|
-
artifactDescriptor(`_compiled/${name}`, 'static', text));
|
|
1881
|
-
const artifacts = [...staticArtifacts, ...functionArtifacts, ...transformArtifacts]
|
|
1882
|
-
.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
|
|
1883
|
-
if (artifacts.length > MAX_ARTIFACT_COUNT) {
|
|
1884
|
-
throw codedError('RESULT_TOO_LARGE', `compiled artifact count ${artifacts.length} > ${MAX_ARTIFACT_COUNT}`);
|
|
1885
|
-
}
|
|
1886
|
-
if (body.artifact_upload && typeof body.artifact_upload.url === 'string' && typeof body.artifact_upload.token === 'string') {
|
|
1887
|
-
const _tUpload = Date.now();
|
|
1888
|
-
if (typeof host.uploadArtifacts !== 'function') {
|
|
1889
|
-
throw codedError('ARTIFACT_UPLOAD_FAILED', 'this compiler host cannot upload artifacts');
|
|
1890
|
-
}
|
|
1891
|
-
uploadedChunks = await host.uploadArtifacts(body.artifact_upload, chunks);
|
|
1892
|
-
timing.upload = _ms(_tUpload);
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
timing.total = _ms(_t0);
|
|
1896
|
-
const dependencyDigest = resolvedDependencyDigest(dependencyReview);
|
|
1897
|
-
return {
|
|
1898
|
-
ok: true,
|
|
1899
|
-
entry_chunk: entryChunk,
|
|
1900
|
-
chunks: uploadedChunks ? {} : chunks,
|
|
1901
|
-
...(uploadedChunks ? { uploaded_chunks: uploadedChunks } : {}),
|
|
1902
|
-
functions: functionBuild.functions,
|
|
1903
|
-
function_graph: functionBuild.graph,
|
|
1904
|
-
transforms,
|
|
1905
|
-
errors: [],
|
|
1906
|
-
source_digest: digest,
|
|
1907
|
-
artifact_manifest: {
|
|
1908
|
-
project_id: body.project_id,
|
|
1909
|
-
build_id: body.build_id,
|
|
1910
|
-
source_digest: digest,
|
|
1911
|
-
dependency_digest: dependencyDigest,
|
|
1912
|
-
compiler: compilerStamp(digest),
|
|
1913
|
-
artifacts,
|
|
1914
|
-
},
|
|
1915
|
-
// The compiler emits Tailwind in the CSS chunk — the worker must not inject
|
|
1916
|
-
// the runtime CDN. Always false; the compiled stylesheet rides in chunks.
|
|
1917
|
-
needs_tailwind: false,
|
|
1918
|
-
// Mirrored at top level (in addition to metadata.warnings) so the worker's
|
|
1919
|
-
// existing bundleProject(), which reads `body.warnings`, is drop-in on the
|
|
1920
|
-
// PR3 cutover without a response remap.
|
|
1921
|
-
warnings,
|
|
1922
|
-
graph: graphFromMetafile(result.metafile, root),
|
|
1923
|
-
metadata: {
|
|
1924
|
-
timing_ms: timing,
|
|
1925
|
-
importGraph: importGraphFromMetafile(result.metafile, root),
|
|
1926
|
-
routes: { static: Object.keys(frontendFiles).filter((item) => item !== '__somewhere_empty_entry.mjs'), functions: functionRoutes },
|
|
1927
|
-
tailwind: twVersion > 0 ? { version: twVersion, compiled: true } : null,
|
|
1928
|
-
typeErrors,
|
|
1929
|
-
typed_functions: {
|
|
1930
|
-
manifest: typedAnalysis.manifest,
|
|
1931
|
-
summary: typedAnalysis.summary,
|
|
1932
|
-
timing_ms: typedAnalysis.timing_ms,
|
|
1933
|
-
},
|
|
1934
|
-
// Dependency review (PR1) — what the customer's resolved tree contains.
|
|
1935
|
-
// Null only if the scan itself threw (a `dependency review skipped`
|
|
1936
|
-
// warning is then present). knownBad already echoed into warnings above.
|
|
1937
|
-
dependency_review: dependencyReview,
|
|
1938
|
-
dependency_digest: dependencyDigest,
|
|
1939
|
-
warnings,
|
|
1940
|
-
// TODO PR4: prerenderedHtml via renderToString in headless Chromium.
|
|
1941
|
-
// TODO: securityFindings, bundleAnalysis.
|
|
1942
|
-
},
|
|
1943
|
-
};
|
|
1944
|
-
} finally {
|
|
1945
|
-
process.chdir(prevCwd);
|
|
1946
|
-
await fsp.rm(root, { recursive: true, force: true }).catch(() => {});
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
return {
|
|
1950
|
-
// The compile contract — identical signatures to the pre-extraction
|
|
1951
|
-
// server.js exports, so the container's consumers and contract tests are
|
|
1952
|
-
// untouched by the move.
|
|
1953
|
-
compile,
|
|
1954
|
-
compileProject,
|
|
1955
|
-
compileTransforms,
|
|
1956
|
-
parseSources,
|
|
1957
|
-
compilerStamp,
|
|
1958
|
-
// Exported for the container's regression fixtures (and reused by the CLI's
|
|
1959
|
-
// parity fixture). See server.js's module.exports for what each one guards.
|
|
1960
|
-
detectTailwind,
|
|
1961
|
-
reviewDependencies,
|
|
1962
|
-
runTypecheck,
|
|
1963
|
-
runReferenceCheck,
|
|
1964
|
-
ensureDeps,
|
|
1965
|
-
bakedSatisfies,
|
|
1966
|
-
react19BakedDir,
|
|
1967
|
-
detectPhantomImports,
|
|
1968
|
-
extractBareSpecifiers,
|
|
1969
|
-
tsconfigAliasMatchers,
|
|
1970
|
-
normalizeTsconfigText,
|
|
1971
|
-
isFunctionPath,
|
|
1972
|
-
sourceDigest,
|
|
1973
|
-
REACT19_NODE_MODULES,
|
|
1974
|
-
IMAGE_NODE_MODULES,
|
|
1975
|
-
};
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
module.exports = {
|
|
1979
|
-
createCompileCore,
|
|
1980
|
-
COMPILER_CONTRACT,
|
|
1981
|
-
// Install-spec derivation (tsk_f79d71ce) — pure, exported for the fixture
|
|
1982
|
-
// that pins the rule in both directions (lockfile present / absent).
|
|
1983
|
-
LOCKFILE_NAMES,
|
|
1984
|
-
lockfileVersions,
|
|
1985
|
-
resolveInstallSpec,
|
|
1986
|
-
cleanRange,
|
|
1987
|
-
// The devDependency carve-out (tsk_2a7c6b33) — pure, exported so the fixture
|
|
1988
|
-
// can pin BOTH directions: the toolchain is dropped, an imported dev-declared
|
|
1989
|
-
// package is not.
|
|
1990
|
-
importedDevDeps,
|
|
1991
|
-
NODE_POLYFILLS,
|
|
1992
|
-
ASSET_LOADERS,
|
|
1993
|
-
KNOWN_BAD_VERSIONS,
|
|
1994
|
-
MAX_TOTAL_SOURCE_BYTES,
|
|
1995
|
-
MAX_FILE_COUNT,
|
|
1996
|
-
MAX_BINARY_BYTES,
|
|
1997
|
-
MAX_ARTIFACT_COUNT,
|
|
1998
|
-
GRAPH_MAX_EDGES,
|
|
1999
|
-
GRAPH_MAX_BYTES,
|
|
2000
|
-
importGraphFromMetafile,
|
|
2001
|
-
graphFromMetafile,
|
|
2002
|
-
isFunctionPath,
|
|
2003
|
-
detectTailwind,
|
|
2004
|
-
reviewDependencies,
|
|
2005
|
-
extractBareSpecifiers,
|
|
2006
|
-
sourceDigest,
|
|
2007
|
-
};
|