@somewhere-tech/cli 0.31.17 → 0.31.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/commands/browser.js +1 -1
  2. package/dist/commands/browser.js.map +1 -1
  3. package/dist/commands/check.js +1 -2
  4. package/dist/commands/check.js.map +1 -1
  5. package/dist/commands/dev.js +18 -383
  6. package/dist/commands/dev.js.map +1 -1
  7. package/dist/commands/status.js +0 -36
  8. package/dist/commands/status.js.map +1 -1
  9. package/dist/commands/typecheck.js +1 -1
  10. package/dist/commands/typecheck.js.map +1 -1
  11. package/dist/commands/verify.js +1 -1
  12. package/dist/commands/verify.js.map +1 -1
  13. package/dist/index.js +0 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/{local → lib}/browser-run.js +1 -1
  16. package/dist/lib/browser-run.js.map +1 -0
  17. package/dist/lib/chrome.js.map +1 -0
  18. package/dist/lib/envfile-write.js +2 -2
  19. package/dist/lib/envfile-write.js.map +1 -1
  20. package/dist/lib/frontend-dev.js +122 -0
  21. package/dist/lib/frontend-dev.js.map +1 -0
  22. package/dist/lib/init-agent-guide.js +15 -19
  23. package/dist/lib/init-agent-guide.js.map +1 -1
  24. package/dist/lib/init-green-template.js +1 -1
  25. package/dist/lib/platform-tools.js +4 -1
  26. package/dist/lib/platform-tools.js.map +1 -1
  27. package/dist/lib/project-urls.js +13 -0
  28. package/dist/lib/project-urls.js.map +1 -1
  29. package/npm-shrinkwrap.json +2 -17
  30. package/package.json +4 -6
  31. package/runtime/VENDOR.json +0 -2
  32. package/dist/commands/exec.js +0 -145
  33. package/dist/commands/exec.js.map +0 -1
  34. package/dist/local/browser-run.js.map +0 -1
  35. package/dist/local/chrome.js.map +0 -1
  36. package/dist/local/compiler-core.js +0 -51
  37. package/dist/local/compiler-core.js.map +0 -1
  38. package/dist/local/compiler.js +0 -683
  39. package/dist/local/compiler.js.map +0 -1
  40. package/dist/local/dev-server.js +0 -517
  41. package/dist/local/dev-server.js.map +0 -1
  42. package/dist/local/envfile.js +0 -40
  43. package/dist/local/envfile.js.map +0 -1
  44. package/dist/local/loader.js +0 -299
  45. package/dist/local/loader.js.map +0 -1
  46. package/dist/local/loopback.js +0 -129
  47. package/dist/local/loopback.js.map +0 -1
  48. package/dist/local/request-timing.js +0 -101
  49. package/dist/local/request-timing.js.map +0 -1
  50. package/dist/local/router.js +0 -114
  51. package/dist/local/router.js.map +0 -1
  52. package/dist/local/runtime.js +0 -345
  53. package/dist/local/runtime.js.map +0 -1
  54. package/dist/local/server.js +0 -309
  55. package/dist/local/server.js.map +0 -1
  56. package/dist/local/telemetry-filter.js +0 -120
  57. package/dist/local/telemetry-filter.js.map +0 -1
  58. package/node_modules/esbuild-wasm/LICENSE.md +0 -21
  59. package/node_modules/esbuild-wasm/README.md +0 -3
  60. package/node_modules/esbuild-wasm/bin/esbuild +0 -91
  61. package/node_modules/esbuild-wasm/esbuild.wasm +0 -0
  62. package/node_modules/esbuild-wasm/esm/browser.d.ts +0 -705
  63. package/node_modules/esbuild-wasm/esm/browser.js +0 -2393
  64. package/node_modules/esbuild-wasm/esm/browser.min.js +0 -20
  65. package/node_modules/esbuild-wasm/lib/browser.d.ts +0 -705
  66. package/node_modules/esbuild-wasm/lib/browser.js +0 -2438
  67. package/node_modules/esbuild-wasm/lib/browser.min.js +0 -22
  68. package/node_modules/esbuild-wasm/lib/main.d.ts +0 -705
  69. package/node_modules/esbuild-wasm/lib/main.js +0 -2051
  70. package/node_modules/esbuild-wasm/package.json +0 -19
  71. package/node_modules/esbuild-wasm/wasm_exec.js +0 -561
  72. package/node_modules/esbuild-wasm/wasm_exec_node.js +0 -39
  73. package/runtime/compiler/VENDOR.json +0 -64
  74. package/runtime/compiler/compile-core.cjs +0 -2007
  75. package/runtime/compiler/graph-contract.cjs +0 -124
  76. package/runtime/compiler/typed-functions.cjs +0 -575
  77. package/runtime/platform-context.mjs +0 -8193
  78. package/runtime/sw-init.mjs +0 -283
  79. /package/dist/{local → lib}/chrome.js +0 -0
@@ -1,683 +0,0 @@
1
- /**
2
- * The platform's compiler, running on your machine.
3
- *
4
- * `somewhere dev` does not build your app with a second toolchain that
5
- * resembles the platform's. It runs the SAME compiler the compile container
6
- * runs on deploy — `runtime/compiler/compile-core.cjs`, vendored verbatim out
7
- * of the monorepo with a drift guard (scripts/extract-runtime.mjs,
8
- * test/compiler-vendor.test.mjs) — so what renders on localhost is the deploy
9
- * artifact, not an approximation of it. The parity fixture
10
- * (test/compiler-parity.test.mjs) asserts that byte for byte.
11
- *
12
- * The compiler is host-parameterized: everything that differs between the
13
- * container and this machine goes in one `host` object. Concretely, three
14
- * things differ.
15
- *
16
- * esbuild. The container runs native esbuild; the CLI runs esbuild-wasm at
17
- * the SAME pinned version. Not a preference — the published CLI's
18
- * supply-chain rule is that every production dependency sits inside the
19
- * signed artifact, and native esbuild ships 24 optional platform packages of
20
- * which only the build machine's own can ever be installed. esbuild-wasm is
21
- * one platform-independent package. Same version in, same bytes out; the
22
- * parity fixture proves it every run.
23
- *
24
- * Where the app's dependencies come from. The container has a baked
25
- * node_modules; you have YOUR node_modules, which is better — it is the
26
- * exact tree you installed. When it is absent or does not satisfy a pin, the
27
- * CLI resolves the way the container does, into a cache under
28
- * ~/.somewhere/dev-deps, silently. See resolveAppDependencies below.
29
- *
30
- * Where the BUILD toolchain comes from. Never your project. The compiler
31
- * treats typescript / postcss / autoprefixer / tailwind as its own
32
- * machinery, not your app's (it deliberately refuses to install them from
33
- * your package.json), so the CLI runs the container's EXACT pins from
34
- * runtime/compiler/VENDOR.json into ~/.somewhere/dev-toolchain. Borrowing
35
- * your tailwind 3.3 or typescript 5.2 would compile different CSS and
36
- * resolve aliases differently — the precise local-vs-deploy divergence this
37
- * whole loop exists to remove.
38
- */
39
- import { execFile } from 'node:child_process';
40
- import { createHash } from 'node:crypto';
41
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
42
- import { createRequire } from 'node:module';
43
- import { homedir } from 'node:os';
44
- import { dirname, join, resolve } from 'node:path';
45
- import { fileURLToPath } from 'node:url';
46
- import { promisify } from 'node:util';
47
- import semver from 'semver';
48
- import { readPlatformModules } from './compiler-core.js';
49
- const execFileAsync = promisify(execFile);
50
- /** A compile that failed, located in the developer's own source. */
51
- export class CompileFailure extends Error {
52
- locations;
53
- constructor(message, locations) {
54
- super(message);
55
- this.locations = locations;
56
- this.name = 'CompileFailure';
57
- }
58
- }
59
- function packageRoot() {
60
- // dist/local/compiler.js → package root is two levels up.
61
- return join(dirname(fileURLToPath(import.meta.url)), '..', '..');
62
- }
63
- /**
64
- * esbuild-wasm, forbidden from writing to its own stderr.
65
- *
66
- * esbuild-wasm is not a library call: it is a child `node` process running the
67
- * WASM build, spoken to over a length-prefixed stdio protocol. That child is
68
- * spawned by esbuild's own code as `stdio: ['pipe', 'pipe', 'inherit']` — its
69
- * stderr is OUR file descriptor 2, whatever ours happens to be.
70
- *
71
- * The child's entry shim monkey-patches `fs.writeSync` to route fd 1 and fd 2
72
- * through `process.stdout.write` / `process.stderr.write`. That patch is only
73
- * sound while those streams are sockets or TTYs. When fd 2 is a plain FILE —
74
- * `somewhere dev 2> dev.log`, any agent or CI harness that captures output to a
75
- * file, a session recorder — node backs `process.stderr` with a SyncWriteStream
76
- * whose `_write` calls `fs.writeSync`, which the patch has redirected back to
77
- * `process.stderr.write`. Each write re-enters the stream it is servicing, the
78
- * pending-write array grows without bound, and `Array.push` finally throws
79
- * `RangeError: Invalid array length` — an unhandled 'error' event that kills the
80
- * child. To the CLI the service simply stopped, and every later rebuild fails
81
- * with "The service is no longer running": one bad import took the loop down for
82
- * the rest of the session (tsk_d63b3b6a).
83
- *
84
- * Which builds write to stderr decides which builds crash, and that is exactly
85
- * the reported split. The core parses every source file with
86
- * `logLevel: 'silent'`, so a SYNTAX error is reported cleanly and the loop
87
- * survives. The bundle step passes no logLevel, so esbuild's default ('warning')
88
- * prints the "Could not resolve" banner — and any warning — to stderr. An
89
- * unresolvable import therefore crashed on every input.
90
- *
91
- * We cannot choose the child's stdio, so we remove its reason to write.
92
- * `logLevel: 'silent'` costs nothing: errors and warnings come back over the
93
- * protocol in `result.errors` / `result.warnings`, which is where the CLI has
94
- * always read them from — the banner was duplicate output that only ever
95
- * appeared on a terminal. This is a host difference, so it belongs here with the
96
- * other three, not in the vendored core.
97
- */
98
- export function silenceEsbuild(esbuild) {
99
- const silent = (options) => ({ ...(options ?? {}), logLevel: 'silent' });
100
- return {
101
- ...esbuild,
102
- build: (options) => esbuild.build(silent(options)),
103
- context: (options) => esbuild.context(silent(options)),
104
- transform: (input, options) => esbuild.transform(input, silent(options)),
105
- };
106
- }
107
- /**
108
- * True when the error says the bridge itself is gone rather than the source is
109
- * bad. esbuild reports both service death and post-mortem calls with these two
110
- * sentences and no structured errors.
111
- */
112
- export function isServiceDeath(err) {
113
- const message = err instanceof Error ? err.message : typeof err === 'string' ? err : '';
114
- return /The service (?:was stopped|is no longer running)/i.test(message);
115
- }
116
- export function readVendorManifest(root = packageRoot()) {
117
- return JSON.parse(readFileSync(join(root, 'runtime', 'compiler', 'VENDOR.json'), 'utf8'));
118
- }
119
- // ─── Dependency resolution, with zero steps for the developer ───────────────
120
- function cacheHome() {
121
- return join(process.env.SOMEWHERE_DEV_CACHE ?? join(homedir(), '.somewhere'), '');
122
- }
123
- /**
124
- * Install `specs` into `dir` as a bare node_modules tree.
125
- *
126
- * `--ignore-scripts` is the same supply-chain guard the container uses, and it
127
- * matters MORE here: this runs on your machine, not in a throwaway container.
128
- * A dependency's postinstall never executes. esbuild bundles a package's files
129
- * without running them, so frontend dependencies do not need install scripts.
130
- */
131
- async function installInto(dir, specs) {
132
- mkdirSync(dir, { recursive: true });
133
- const manifest = join(dir, 'package.json');
134
- if (!existsSync(manifest)) {
135
- writeFileSync(manifest, JSON.stringify({ name: 'somewhere-dev-cache', private: true }, null, 2));
136
- }
137
- try {
138
- await execFileAsync('npm', ['install', '--ignore-scripts', '--no-bin-links', '--no-audit', '--no-fund',
139
- '--no-package-lock', '--prefix', dir, ...specs], { cwd: dir, maxBuffer: 32 * 1024 * 1024, env: { ...process.env, npm_config_ignore_scripts: 'true' } });
140
- }
141
- catch (err) {
142
- const detail = err instanceof Error
143
- ? (err.stderr ?? err.message).toString().slice(-600)
144
- : String(err);
145
- throw new Error(`Could not resolve ${specs.join(', ')}: ${detail}`);
146
- }
147
- }
148
- /** True when `dir/node_modules/<name>` is a package esbuild would resolve. */
149
- function installed(dir, name) {
150
- return existsSync(join(dir, 'node_modules', name, 'package.json'));
151
- }
152
- /**
153
- * The build toolchain, at the container's exact pins, in the CLI's cache.
154
- *
155
- * One directory per group because tailwind v3 and v4 are the same package NAME
156
- * at incompatible majors — the container isolates them for that reason and so
157
- * does this. Groups are prepared only when the project needs them: a project
158
- * with no Tailwind never installs a Tailwind engine.
159
- *
160
- * Returns the group directories, already populated.
161
- */
162
- async function prepareToolchain(manifest, groups, onFirstInstall) {
163
- const dirs = {};
164
- for (const group of groups) {
165
- const pins = manifest.toolchain[group];
166
- if (!pins)
167
- throw new Error(`the vendored compiler declares no "${group}" toolchain group`);
168
- const dir = join(cacheHome(), 'dev-toolchain', `${group}-${digestOf(pins)}`);
169
- dirs[group] = dir;
170
- const missing = Object.entries(pins)
171
- .filter(([name]) => !installed(dir, name))
172
- .map(([name, range]) => `${name}@${range}`);
173
- if (!missing.length)
174
- continue;
175
- onFirstInstall?.(`build toolchain (${missing.map((s) => s.split('@')[0] || s).join(', ')})`);
176
- await installInto(dir, missing);
177
- }
178
- return dirs;
179
- }
180
- /**
181
- * Install a dependency at the version the compile image BAKES, not the floor of
182
- * the declared range (tsk_0312cf17 for React, tsk_a8cb3d23 for the rest of the
183
- * baked set).
184
- *
185
- * The image keeps React 19 in a tree of its own and prefers it over installing
186
- * the app's range; locally that set does not exist, so the compiler took its
187
- * other branch and floor-pinned instead — `react: ^19.2.0` became exactly
188
- * 19.2.0 while the image served 19.2.7. The same tree, compiled by the same
189
- * compiler, against two different Reacts.
190
- *
191
- * That was never only about React. The image bakes ~20 more packages (router,
192
- * query, forms, validation, dates, charts, icons…), and `ensureDeps` prefers a
193
- * baked copy whenever its version satisfies the declared range — so `zod:
194
- * ^3.23.0` served the image's baked 3.24.x on deploy and a floor-pinned 3.23.0
195
- * locally. Same divergence, same fix: every group the vendor records is offered
196
- * here, in the container's own order (baked first, then the isolated React 19
197
- * set), each guarded by imagePinSatisfies so a pin is only ever taken when the
198
- * container itself would have taken the baked copy.
199
- *
200
- * WHAT THIS DOES NOT COVER, stated plainly: when the project has its own
201
- * node_modules, that tree is first on the search path and the local build uses
202
- * the copy the developer installed. Deliberate — it is the exact tree they
203
- * installed, and it is why a local loop beats a cloud one. These pins decide
204
- * what the CLI's own dependency cache installs when a package is NOT already
205
- * on the search path.
206
- *
207
- * The fix is to rewrite the spec, NOT to add a second node_modules tree. That
208
- * was tried and is actively worse: a package that physically lives inside the
209
- * dependency cache resolves `react` to its own sibling before any search path
210
- * is consulted, so a separate pinned tree gets bundled ALONGSIDE the cache's
211
- * copy — two Reacts in one bundle, which renders a blank page. The image has no
212
- * such problem because it ends up with one flat tree, and this keeps one too.
213
- *
214
- * Pinning the spec also stops npm resolving React on its own as a peer of
215
- * something else (react-router-dom pulls one in), which is how a warm cache
216
- * ended up serving a third version again.
217
- */
218
- export function applyImagePins(specs, deps, pinGroups) {
219
- const groups = pinGroups.filter((g) => g && Object.keys(g).length > 0);
220
- if (!groups.length)
221
- return specs;
222
- return specs.map((spec) => {
223
- const at = spec.lastIndexOf('@');
224
- const name = at > 0 ? spec.slice(0, at) : spec;
225
- const range = deps[name];
226
- for (const group of groups) {
227
- const version = group[name];
228
- if (version && imagePinSatisfies(version, range))
229
- return `${name}@${version}`;
230
- }
231
- return spec;
232
- });
233
- }
234
- /**
235
- * May the image's baked VERSION stand in for the project's declared RANGE?
236
- *
237
- * This is `bakedSatisfies` from the compile core, asked from the other side:
238
- * the container prefers its baked copy only when that copy satisfies the range,
239
- * so the local loop may pin to it only under the same condition. Without the
240
- * check a project that declares `react-router-dom: ^7.9.5` would be pinned to
241
- * the image's baked 6.x — a build that works today refused tomorrow.
242
- *
243
- * The bias is the core's, one-directional: anything we cannot POSITIVELY prove
244
- * satisfies — an unparseable range (`latest`, a git or alias spec), a version
245
- * semver rejects, semver itself unavailable — returns false and leaves the
246
- * core's own spec alone. A false negative costs an install of the floor-pin,
247
- * which is exactly today's behaviour; a false positive would compile the
248
- * developer's app against a library they did not ask for.
249
- */
250
- export function imagePinSatisfies(version, range) {
251
- const wanted = typeof range === 'string' && range.trim() ? range.trim() : '*';
252
- try {
253
- if (semver.validRange(wanted) === null)
254
- return false;
255
- return semver.satisfies(version, wanted, { includePrerelease: false });
256
- }
257
- catch {
258
- return false;
259
- }
260
- }
261
- function digestOf(value) {
262
- return createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 12);
263
- }
264
- /**
265
- * Where the APP's own dependencies come from, in resolution order.
266
- *
267
- * 1. The project's own node_modules, when it exists. This is the whole reason
268
- * a local loop can beat a cloud one: the tree is already on disk and it is
269
- * the exact tree the developer installed.
270
- * 2. A CLI-managed cache keyed by the project's dependency map, for the case
271
- * where node_modules is absent (a fresh `somewhere init`, a clone with no
272
- * install) or a declared pin is not satisfied by what is installed.
273
- *
274
- * Keying the cache on the dependency map rather than sharing one flat tree
275
- * means two projects on incompatible majors of the same package never
276
- * overwrite each other, and two projects with identical dependencies share the
277
- * install for free.
278
- */
279
- function resolveAppDependencies(cwd, pkg, pinGroups) {
280
- const search = [];
281
- const projectModules = join(cwd, 'node_modules');
282
- if (existsSync(projectModules))
283
- search.push(projectModules);
284
- const deps = pkg.dependencies ?? {};
285
- // The pins are part of the cache identity (tsk_0312cf17): when the image
286
- // bumps a baked package and the CLI re-vendors, the old cache is simply not
287
- // reused, so a warm cache can never keep serving the version we moved off.
288
- const cacheDir = join(cacheHome(), 'dev-deps', digestOf([Object.entries(deps).sort(), pinGroups]));
289
- // The core decides what is actually missing (its baked-satisfies rules run
290
- // against this exact search path) and calls host.installPackages for the
291
- // rest. All we do here is make the cache dir part of the search path.
292
- search.push(join(cacheDir, 'node_modules'));
293
- return search;
294
- }
295
- // ─── Entry detection — the same rule the deploy pipeline uses ───────────────
296
- //
297
- // The HTML's `<script type="module" src>` tags are the single source of truth
298
- // for what the compiler is allowed to touch. Mirrors
299
- // worker/src/utils/module-entry.ts; a divergence here would compile a
300
- // different entry locally than on deploy, which is the one thing this loop
301
- // must never do.
302
- export function isCompilableEntry(path) {
303
- return /\.(?:tsx?|jsx|mts|cts)$/i.test(path);
304
- }
305
- export function collectModuleEntryScripts(files) {
306
- const entries = new Set();
307
- for (const [path, content] of Object.entries(files)) {
308
- if (typeof content !== 'string')
309
- continue;
310
- if (!path.toLowerCase().endsWith('.html'))
311
- continue;
312
- const tagRe = /<script\b[^>]*>/gi;
313
- let m;
314
- while ((m = tagRe.exec(content)) !== null) {
315
- const tag = m[0];
316
- if (!/\btype\s*=\s*["']module["']/i.test(tag))
317
- continue;
318
- const srcMatch = /\bsrc\s*=\s*["']([^"']+)["']/i.exec(tag);
319
- if (!srcMatch)
320
- continue;
321
- let src = srcMatch[1];
322
- if (src.startsWith('/'))
323
- src = src.slice(1);
324
- if (isCompilableEntry(src) && files[src] !== undefined)
325
- entries.add(src);
326
- }
327
- }
328
- return entries;
329
- }
330
- export function detectBundleEntry(files) {
331
- const entries = collectModuleEntryScripts(files);
332
- const indexHtml = files['index.html'];
333
- if (typeof indexHtml === 'string') {
334
- for (const src of moduleScriptSrcs(indexHtml)) {
335
- if (entries.has(src))
336
- return src;
337
- }
338
- }
339
- for (const e of entries)
340
- return e;
341
- return null;
342
- }
343
- /** Every `<script type="module" src>` in one HTML document, root-relative
344
- * paths normalized to deploy keys. Shared by every entry rule below so they
345
- * cannot disagree about what counts as a module script. */
346
- function moduleScriptSrcs(html) {
347
- const out = [];
348
- const tagRe = /<script\b[^>]*>/gi;
349
- let m;
350
- while ((m = tagRe.exec(html)) !== null) {
351
- const tag = m[0];
352
- if (!/\btype\s*=\s*["']module["']/i.test(tag))
353
- continue;
354
- const srcMatch = /\bsrc\s*=\s*["']([^"']+)["']/i.exec(tag);
355
- if (!srcMatch)
356
- continue;
357
- let src = srcMatch[1];
358
- if (src.startsWith('/'))
359
- src = src.slice(1);
360
- out.push(src);
361
- }
362
- return out;
363
- }
364
- /**
365
- * A module entry the platform serves EXACTLY as written: plain JavaScript.
366
- *
367
- * Deliberately the complement of isCompilableEntry, not an oversight in it.
368
- * The deploy pipeline compiles the TypeScript family and .jsx and serves
369
- * .js/.mjs/.cjs raw as native ESM, which is what protects browser-Babel
370
- * projects from being compiled twice. Both are entries; they differ only in
371
- * whether the platform compiles them.
372
- */
373
- export function isRawServedEntry(path) {
374
- return /\.[mc]?js$/i.test(path);
375
- }
376
- /** The client entry forms `somewhere dev` and `somewhere deploy` both accept,
377
- * for the message a project with no usable entry gets. */
378
- export const ACCEPTED_ENTRY_FORMS = '.tsx / .ts / .jsx / .mts / .cts are compiled by the platform; .js / .mjs / .cjs are served as written';
379
- /**
380
- * What the local loop should do with this project's frontend, decided from
381
- * index.html exactly as the deploy pipeline decides it.
382
- *
383
- * `somewhere dev` used to refuse any project whose entry was not compilable,
384
- * so a project with `<script type="module" src="/src/main.js">` could be
385
- * deployed and served but not run locally (pfb_e32a4e630c45). Deploy does not
386
- * refuse it — `bundleProject` simply returns null and the file is served raw
387
- * — and the local loop's whole promise is that it is the same build. So:
388
- *
389
- * compiled → index.html points at a TS-family/.jsx entry: compile it.
390
- * raw → index.html points at a .js/.mjs/.cjs entry that exists on
391
- * disk: serve it untouched, which is what deploy does.
392
- * none → index.html declares module scripts but none of them is a file
393
- * in this directory. `declared` carries what it asked for, so
394
- * the message can name the missing path.
395
- *
396
- * An index.html with NO module script at all is `raw` with no entry to name:
397
- * a browser-Babel page or a plain static site is a legitimate project that
398
- * deploys and must therefore run here too.
399
- */
400
- export function resolveDevEntry(files) {
401
- const compiled = detectBundleEntry(files);
402
- if (compiled)
403
- return { kind: 'compiled', entry: compiled };
404
- const htmlFiles = Object.entries(files).filter(([path, content]) => typeof content === 'string' && path.toLowerCase().endsWith('.html'));
405
- // index.html first — it is the document the deploy serves at the root.
406
- htmlFiles.sort(([a], [b]) => (a === 'index.html' ? -1 : b === 'index.html' ? 1 : 0));
407
- const declared = [];
408
- for (const [, content] of htmlFiles) {
409
- for (const src of moduleScriptSrcs(content)) {
410
- declared.push(src);
411
- if (isRawServedEntry(src) && files[src] !== undefined)
412
- return { kind: 'raw', entry: src };
413
- }
414
- }
415
- if (declared.length)
416
- return { kind: 'none', declared };
417
- return { kind: 'raw', entry: '' };
418
- }
419
- /**
420
- * Point index.html at the compiled bundle instead of the raw entry, and add a
421
- * <link> for each CSS chunk esbuild split out. The deploy pipeline does exactly
422
- * this rewrite before serving (jsx-compile.ts) — the browser must receive the
423
- * same HTML locally or the page it renders is not the page deploy renders.
424
- */
425
- export function rewriteIndexHtml(html, entry, entryChunk, chunkNames) {
426
- const escaped = entry.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
427
- const reSourceEntry = new RegExp(`(<script\\b[^>]*\\bsrc\\s*=\\s*)(["'])/?${escaped}\\2`, 'i');
428
- const reCompiledEntry = /(<script\b[^>]*\bsrc\s*=\s*)(["'])\/_compiled\/[^"']+\.js\2/i;
429
- const newUrl = `/_compiled/${entryChunk}`;
430
- let next = html.replace(reSourceEntry, `$1$2${newUrl}$2`);
431
- if (next === html)
432
- next = html.replace(reCompiledEntry, `$1$2${newUrl}$2`);
433
- const cssChunks = chunkNames.filter((name) => name.endsWith('.css'));
434
- const reCompiledCssLink = /<link\b[^>]*\brel\s*=\s*(["'])stylesheet\1[^>]*\bhref\s*=\s*(["'])\/_compiled\/[^"']+\.css\2[^>]*>/gi;
435
- next = next.replace(reCompiledCssLink, '');
436
- if (cssChunks.length) {
437
- const linkTags = cssChunks.map((name) => `<link rel="stylesheet" href="/_compiled/${name}">`).join('\n');
438
- const headClose = /<\/head\s*>/i;
439
- const bodyOpen = /<body\b/i;
440
- if (headClose.test(next))
441
- next = next.replace(headClose, `${linkTags}\n</head>`);
442
- else if (bodyOpen.test(next))
443
- next = next.replace(bodyOpen, `${linkTags}\n<body`);
444
- else
445
- next = `${linkTags}\n${next}`;
446
- }
447
- return next;
448
- }
449
- export class LocalCompiler {
450
- opts;
451
- core = null;
452
- searchPath = [];
453
- depCacheDir = '';
454
- esbuild = null;
455
- constructor(opts) {
456
- this.opts = opts;
457
- }
458
- /**
459
- * Resolve everything the compiler needs before the first build, so every
460
- * rebuild afterwards is pure compile time. Idempotent.
461
- */
462
- /**
463
- * The node_modules dirs the compiler resolves the app's dependencies from,
464
- * in order. The local FUNCTION runtime resolves against the same list, so a
465
- * function and a component import the same copy of a package (tsk_3269026d).
466
- * Empty until prepare() has run.
467
- */
468
- get moduleSearchPath() {
469
- return this.searchPath;
470
- }
471
- async prepare(pkg, tailwindVersion) {
472
- if (this.core)
473
- return;
474
- const root = packageRoot();
475
- const manifest = readVendorManifest(root);
476
- const requireVendored = createRequire(join(root, 'runtime', 'compiler', 'compile-core.cjs'));
477
- const requireCli = createRequire(join(root, 'package.json'));
478
- const groups = ['base', ...(tailwindVersion === 4 ? ['tw4'] : tailwindVersion === 3 ? ['tw3'] : [])];
479
- const toolchainDirs = await prepareToolchain(manifest, groups, this.opts.onPrepare);
480
- // The image's own versions, in the container's preference order: the baked
481
- // tree first, the isolated React 19 set second (ensureDeps only reaches
482
- // react19 when the baked React 18 does not satisfy the pin).
483
- const imagePinGroups = [manifest.toolchain.baked ?? {}, manifest.toolchain.react19 ?? {}];
484
- this.searchPath = resolveAppDependencies(this.opts.cwd, pkg, imagePinGroups);
485
- this.depCacheDir = resolve(this.searchPath[this.searchPath.length - 1], '..');
486
- const requireToolchain = (group) => createRequire(join(toolchainDirs[group], 'package.json'));
487
- const requireBase = requireToolchain('base');
488
- const requireTw3 = toolchainDirs.tw3 ? requireToolchain('tw3') : null;
489
- const requireTw4 = toolchainDirs.tw4 ? requireToolchain('tw4') : null;
490
- const { createCompileCore } = requireVendored('./compile-core.cjs');
491
- // esbuild-wasm at the container's exact version. Loaded through the CLI's
492
- // own require so it comes from the CLI's bundled tree, never the project's,
493
- // and silenced so the WASM child never writes to an inherited stderr —
494
- // see silenceEsbuild.
495
- this.esbuild = requireCli('esbuild-wasm');
496
- this.core = createCompileCore({
497
- esbuild: silenceEsbuild(this.esbuild),
498
- imageNodeModules: this.searchPath,
499
- // No isolated React-19 set locally: it exists in the image to skip a cold
500
- // install of react/react-dom, and here those either sit in the project's
501
- // node_modules already or land in the dependency cache once and stay.
502
- react19NodeModules: null,
503
- tw4TailwindDir: toolchainDirs.tw4 ? join(toolchainDirs.tw4, 'node_modules', 'tailwindcss') : null,
504
- requireImage: (spec) => {
505
- // tailwindcss means v3 here, exactly as it does in the image; the v4
506
- // engine is reached only through requireTw4.
507
- if (spec === 'tailwindcss' && requireTw3)
508
- return requireTw3(spec);
509
- // semver rides along in the CLI's own dependencies already.
510
- if (spec === 'semver')
511
- return requireCli(spec);
512
- return requireBase(spec);
513
- },
514
- requireTw4: requireTw4 ? (spec) => requireTw4(spec) : undefined,
515
- // The container refuses to install without its per-build scoped proxy
516
- // because it is egress-locked. This is the developer's own machine
517
- // running their own npm; the proxy does not exist here and its absence
518
- // is not a safety signal.
519
- requiresPackageProxy: false,
520
- installPackages: async ({ specs }) => {
521
- const pinned = applyImagePins(specs, pkg.dependencies ?? {}, imagePinGroups);
522
- this.opts.onPrepare?.(`${pinned.length} ${pinned.length === 1 ? 'dependency' : 'dependencies'} (${pinned.join(', ')})`);
523
- await installInto(this.depCacheDir, pinned);
524
- },
525
- // The local loop is not a publication: no artifact upload capability, and
526
- // an identity that says plainly where this build came from.
527
- stamp: { source: `cli-vendored-${manifest.commit}`, toolchain: digestOf(manifest.files) },
528
- });
529
- }
530
- /** Compile the project. Throws CompileFailure for an error in the developer's source. */
531
- async compile(sources) {
532
- const { files, binaryFiles } = sources;
533
- const entry = detectBundleEntry(files);
534
- if (!entry) {
535
- throw new CompileFailure('No app entry found. index.html needs a <script type="module" src="/src/main.tsx"> pointing at your app entry — that tag is what the platform compiles, locally and on deploy.', []);
536
- }
537
- const transformEntries = [...collectModuleEntryScripts(files)].filter((path) => path !== entry);
538
- if (!this.core)
539
- throw new Error('LocalCompiler.prepare() must run before compile()');
540
- const body = {
541
- project_id: 'somewhere-dev-local',
542
- build_id: 'somewhere-dev-local',
543
- entry,
544
- files,
545
- binary_files: binaryFiles,
546
- function_entries: [],
547
- transform_entries: transformEntries,
548
- package_json: files['package.json'],
549
- tsconfig: files['tsconfig.json'],
550
- vite_env: this.opts.viteEnv ?? {},
551
- };
552
- let result;
553
- try {
554
- result = await this.core.compile(body);
555
- }
556
- catch (err) {
557
- // A dead bridge is not the developer's bug and must not end the session.
558
- // esbuild caches one long-lived service per process, so once its child
559
- // is gone EVERY later rebuild fails with the same sentence until the
560
- // service is dropped. Drop it and build again: one save should never cost
561
- // more than one build (tsk_d63b3b6a).
562
- if (!isServiceDeath(err))
563
- throw toCompileFailure(err);
564
- try {
565
- this.esbuild?.stop?.();
566
- }
567
- catch {
568
- // stop() is best-effort teardown of an already-dead child.
569
- }
570
- try {
571
- result = await this.core.compile(body);
572
- }
573
- catch (retryErr) {
574
- if (!isServiceDeath(retryErr))
575
- throw toCompileFailure(retryErr);
576
- throw new CompileFailure('The local compiler service stopped and could not be restarted. Re-run `somewhere dev`; if it keeps happening, `somewhere feedback` with this message.', []);
577
- }
578
- }
579
- if (!result.entry_chunk)
580
- throw new CompileFailure('The compiler produced no entry chunk.', []);
581
- const html = typeof files['index.html'] === 'string'
582
- ? rewriteIndexHtml(files['index.html'], entry, result.entry_chunk, Object.keys(result.chunks))
583
- : null;
584
- return {
585
- entryChunk: result.entry_chunk,
586
- chunks: result.chunks,
587
- html,
588
- entry,
589
- warnings: result.warnings ?? [],
590
- sourceDigest: result.source_digest,
591
- artifacts: result.artifact_manifest.artifacts,
592
- };
593
- }
594
- }
595
- /**
596
- * Turn a compiler error into something with a file and a line.
597
- *
598
- * The compiler reports a syntax error as SOURCE_PARSE_ERROR with every bad
599
- * file joined into one message (`src/App.tsx: Expected ";" ...`). esbuild's own
600
- * errors carry a structured location. Both become a list of
601
- * {file, line, column, text} so the terminal and the browser overlay can point
602
- * at the exact line instead of printing a paragraph.
603
- */
604
- /**
605
- * Root package name for a specifier: `@scope/pkg/sub` → `@scope/pkg`,
606
- * `pkg/sub` → `pkg`. Mirrors the vendored core's specRootPkg, so the CLI and
607
- * the compiler agree on what "the package" is in an import.
608
- */
609
- function rootPackage(spec) {
610
- return spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
611
- }
612
- /**
613
- * One sentence saying what to do about an unresolvable import.
614
- *
615
- * Two cases, and they need opposite advice. A normal npm package is missing
616
- * from the tree and `npm install` fixes it. A PLATFORM specifier is not on npm
617
- * at all — `npm install somewhere` fetches an unrelated package — so the fix is
618
- * always the import path, never an install. Which is which comes from the
619
- * vendored compiler's own PLATFORM_MODULES list (readPlatformModules), the same
620
- * enumeration that builds the compiler's virtual-module resolver, so the CLI
621
- * cannot drift into calling one of our own modules a missing dependency.
622
- *
623
- * Returns undefined for anything that is not an unresolved-import error, which
624
- * leaves every other message exactly as esbuild wrote it.
625
- */
626
- export function resolutionHint(text) {
627
- const match = /^Could not resolve "([^"]+)"/.exec(text);
628
- if (!match)
629
- return undefined;
630
- const spec = match[1];
631
- const platformRoots = new Set(readPlatformModules().map(rootPackage));
632
- if (platformRoots.has(rootPackage(spec)) || readPlatformModules().includes(spec)) {
633
- return `\`${spec}\` is provided by the platform, not npm — do not add it to package.json. Check the import path: platform modules are only available where the platform supplies them (\`somewhere/db\` belongs in db/schema.ts, which the deploy reads as data and never bundles).`;
634
- }
635
- return `Add \`${rootPackage(spec)}\` to the dependencies in your package.json and run \`npm install\`.`;
636
- }
637
- export function toCompileFailure(err) {
638
- const locations = [];
639
- // The compiler's own parse phase hands back {file, line, column, message}
640
- // per bad file (source_errors). Use it verbatim — it is the most precise
641
- // location available, and it is what a syntax error produces.
642
- const sourceErrors = err?.source_errors;
643
- if (Array.isArray(sourceErrors) && sourceErrors.length) {
644
- for (const e of sourceErrors) {
645
- locations.push({ file: e.file, line: e.line, column: e.column, text: e.message });
646
- }
647
- return new CompileFailure(sourceErrors[0].message, locations);
648
- }
649
- const esbuildErrors = err?.errors;
650
- if (Array.isArray(esbuildErrors) && esbuildErrors.length) {
651
- for (const e of esbuildErrors) {
652
- locations.push({
653
- file: e.location?.file ?? '',
654
- line: e.location?.line,
655
- column: e.location ? e.location.column + 1 : undefined,
656
- text: e.text,
657
- hint: resolutionHint(e.text),
658
- });
659
- }
660
- return new CompileFailure(esbuildErrors[0].text, locations);
661
- }
662
- const message = err instanceof Error ? err.message : String(err);
663
- for (const part of message.split('; ')) {
664
- // `src/App.tsx:12:4: Expected ";" but found "}"` and the shorter
665
- // `src/App.tsx: Expected ...` the parse phase produces.
666
- const withPos = /^([^\s:]+\.[a-z]+):(\d+):(\d+):\s*(.+)$/i.exec(part);
667
- if (withPos) {
668
- locations.push({
669
- file: withPos[1],
670
- line: Number(withPos[2]),
671
- column: Number(withPos[3]),
672
- text: withPos[4],
673
- hint: resolutionHint(withPos[4]),
674
- });
675
- continue;
676
- }
677
- const fileOnly = /^([^\s:]+\.[a-z]+):\s*(.+)$/i.exec(part);
678
- if (fileOnly)
679
- locations.push({ file: fileOnly[1], text: fileOnly[2], hint: resolutionHint(fileOnly[2]) });
680
- }
681
- return new CompileFailure(message, locations);
682
- }
683
- //# sourceMappingURL=compiler.js.map