@_linked/cli 1.31.0 → 1.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +15 -0
  3. package/defaults/app-react-native/packages/app-shapes/src/index.ts +2 -0
  4. package/defaults/app-react-native/packages/app-shapes/src/shapes/index.ts +5 -0
  5. package/defaults/package/src/shapes/index.ts +3 -1
  6. package/lib/esm/cli-methods.d.ts.map +1 -1
  7. package/lib/esm/cli-methods.js +144 -118
  8. package/lib/esm/cli-methods.js.map +1 -1
  9. package/lib/esm/cli.js +7 -0
  10. package/lib/esm/cli.js.map +1 -1
  11. package/lib/esm/client-dep-includes.d.ts +144 -0
  12. package/lib/esm/client-dep-includes.d.ts.map +1 -0
  13. package/lib/esm/client-dep-includes.js +827 -0
  14. package/lib/esm/client-dep-includes.js.map +1 -0
  15. package/lib/esm/commands/doctor.d.ts +19 -0
  16. package/lib/esm/commands/doctor.d.ts.map +1 -0
  17. package/lib/esm/commands/doctor.js +167 -0
  18. package/lib/esm/commands/doctor.js.map +1 -0
  19. package/lib/esm/installed-packages.d.ts +18 -0
  20. package/lib/esm/installed-packages.d.ts.map +1 -0
  21. package/lib/esm/installed-packages.js +66 -0
  22. package/lib/esm/installed-packages.js.map +1 -0
  23. package/lib/esm/utils/shapeReferences.d.ts +10 -7
  24. package/lib/esm/utils/shapeReferences.d.ts.map +1 -1
  25. package/lib/esm/utils/shapeReferences.js +13 -18
  26. package/lib/esm/utils/shapeReferences.js.map +1 -1
  27. package/lib/esm/utils/shapesIndex.d.ts +21 -0
  28. package/lib/esm/utils/shapesIndex.d.ts.map +1 -0
  29. package/lib/esm/utils/shapesIndex.js +204 -0
  30. package/lib/esm/utils/shapesIndex.js.map +1 -0
  31. package/lib/esm/vite-config.d.ts +37 -0
  32. package/lib/esm/vite-config.d.ts.map +1 -1
  33. package/lib/esm/vite-config.js +70 -51
  34. package/lib/esm/vite-config.js.map +1 -1
  35. package/package.json +5 -2
@@ -0,0 +1,827 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ var _a;
11
+ var _b, _c;
12
+ // `optimizeDeps.include` entries for the third-party client dependencies of
13
+ // linked framework packages.
14
+ //
15
+ // Apps exclude the framework packages (`@_linked/*`, `lincd-*`, anything marked
16
+ // `"linkedPackage": true`) from Vite's dependency optimizer, so the browser
17
+ // loads exactly one copy of each. The cost of that exclusion: Vite no longer
18
+ // crawls into those packages, so it never learns about THEIR dependencies until
19
+ // the browser happens to request one. Two things then go wrong:
20
+ //
21
+ // - a CommonJS dependency (`use-sync-external-store/shim`, `js-cookie`) is
22
+ // served raw, and the browser fails on `require`/`module.exports`;
23
+ // - any other dependency is discovered mid-session, and Vite re-optimises and
24
+ // reloads the page — which can mix old and new chunks into two Reacts.
25
+ //
26
+ // So apps used to list every such dependency in `optimizeDeps.include` by hand,
27
+ // including nested installs (`'@_linked/primitives > vaul'`, where npm put vaul
28
+ // under primitives because its React peer range excluded the app's React).
29
+ //
30
+ // Reading `package.json` dependencies would list server-only packages too
31
+ // (bcrypt, express, sharp…), which then fail in esbuild. This module reads the
32
+ // import graph instead: which bare specifiers the framework packages' client
33
+ // entry files actually import, and which of those can run in a browser.
34
+ import fs from 'node:fs/promises';
35
+ import path from 'node:path';
36
+ import { isBuiltin } from 'node:module';
37
+ import { init as initLexer, parse as parseImports } from 'es-module-lexer';
38
+ import { isFrameworkPkg, readInstalledPkg } from './installed-packages.js';
39
+ /** Conditions a browser build resolves, in the order a client resolver prefers them. */
40
+ const CLIENT_CONDITIONS = ['browser', 'import', 'module', 'default'];
41
+ /** Conditions never taken for a client file. `development` points at `src/`, scanned separately. */
42
+ const NON_CLIENT_CONDITIONS = new Set([
43
+ 'require',
44
+ 'node',
45
+ 'types',
46
+ 'typings',
47
+ 'development',
48
+ 'deno',
49
+ 'bun',
50
+ 'worker',
51
+ 'react-native',
52
+ ]);
53
+ /** Specifiers that are assets, not modules the optimizer can pre-bundle. */
54
+ const ASSET_SPECIFIER = /\.(css|scss|sass|less|styl|pcss|svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|otf|eot|json|wasm|html?|txt|md|glsl)$/i;
55
+ const SOURCE_FILE = /\.(m?[jt]sx?|cjs)$/;
56
+ const IGNORED_SOURCE_FILE = /(\.d\.ts|\.(test|spec|stories)\.[mc]?[jt]sx?)$/;
57
+ const IGNORED_SOURCE_DIR = new Set([
58
+ 'node_modules',
59
+ '__tests__',
60
+ '__mocks__',
61
+ '__fixtures__',
62
+ ]);
63
+ /**
64
+ * Never listed, whoever imports them. `typescript` is a peer of packages that
65
+ * analyse source at build time (translation key sync); it declares a `browser`
66
+ * field, so it passes the server-only test, and pre-bundling it costs seconds.
67
+ */
68
+ export const DEFAULT_DENY = ['typescript'];
69
+ /** A server-only check stops after this many files of one dependency. */
70
+ const MAX_DEP_FILES = 400;
71
+ /** `@scope/name/sub` → `@scope/name`; `name/sub` → `name`. */
72
+ export function packageNameOf(specifier) {
73
+ const parts = specifier.split('/');
74
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
75
+ }
76
+ /** A bare module specifier naming another package (not relative, builtin, virtual, asset or `#import`). */
77
+ export function isBarePackageSpecifier(specifier) {
78
+ if (!specifier || specifier.startsWith('.') || specifier.startsWith('/'))
79
+ return false;
80
+ if (specifier.startsWith('#') || specifier.startsWith('\0'))
81
+ return false;
82
+ if (specifier.includes(':') || specifier.includes('?'))
83
+ return false; // node:, virtual:, http:, ?raw
84
+ if (isBuiltin(specifier))
85
+ return false;
86
+ if (ASSET_SPECIFIER.test(specifier))
87
+ return false;
88
+ return /^(@[\w.-]+\/)?[\w.-]/.test(specifier);
89
+ }
90
+ const matchesDeny = (deny, ...values) => deny.some((d) => values.some((v) => (typeof d === 'string' ? d === v : d.test(v))));
91
+ function realpath(p) {
92
+ return __awaiter(this, void 0, void 0, function* () {
93
+ try {
94
+ return yield fs.realpath(p);
95
+ }
96
+ catch (_a) {
97
+ return path.resolve(p);
98
+ }
99
+ });
100
+ }
101
+ function isFile(p) {
102
+ return __awaiter(this, void 0, void 0, function* () {
103
+ try {
104
+ return (yield fs.stat(p)).isFile();
105
+ }
106
+ catch (_a) {
107
+ return false;
108
+ }
109
+ });
110
+ }
111
+ function listFiles(dir, accept) {
112
+ return __awaiter(this, void 0, void 0, function* () {
113
+ const out = [];
114
+ const walk = (d) => __awaiter(this, void 0, void 0, function* () {
115
+ let entries;
116
+ try {
117
+ entries = yield fs.readdir(d, { withFileTypes: true });
118
+ }
119
+ catch (_a) {
120
+ return;
121
+ }
122
+ yield Promise.all(entries.map((ent) => __awaiter(this, void 0, void 0, function* () {
123
+ const full = path.join(d, ent.name);
124
+ if (ent.isDirectory()) {
125
+ if (!IGNORED_SOURCE_DIR.has(ent.name))
126
+ yield walk(full);
127
+ }
128
+ else if (accept(full)) {
129
+ out.push(full);
130
+ }
131
+ })));
132
+ });
133
+ yield walk(dir);
134
+ return out;
135
+ });
136
+ }
137
+ /** Pick the target of one export entry for the given conditions (nested condition objects allowed). */
138
+ function pickConditional(target, conditions) {
139
+ if (typeof target === 'string')
140
+ return target;
141
+ if (Array.isArray(target)) {
142
+ for (const t of target) {
143
+ const hit = pickConditional(t, conditions);
144
+ if (hit)
145
+ return hit;
146
+ }
147
+ return null;
148
+ }
149
+ if (target && typeof target === 'object') {
150
+ for (const [key, value] of Object.entries(target)) {
151
+ if (key === 'default' || conditions.includes(key)) {
152
+ const hit = pickConditional(value, conditions);
153
+ if (hit)
154
+ return hit;
155
+ }
156
+ }
157
+ }
158
+ return null;
159
+ }
160
+ /** Normalise `exports` to a subpath map (`{'.': …}`), or null when there is none. */
161
+ function exportsMap(exportsField) {
162
+ if (exportsField === undefined || exportsField === null)
163
+ return null;
164
+ if (typeof exportsField === 'string' || Array.isArray(exportsField))
165
+ return { '.': exportsField };
166
+ const keys = Object.keys(exportsField);
167
+ if (keys.length > 0 && keys.every((k) => !k.startsWith('.')))
168
+ return { '.': exportsField };
169
+ return exportsField;
170
+ }
171
+ /**
172
+ * Resolve a package subpath (`.` or `./shim`) through `exports` for the given
173
+ * conditions, the way Node does (exact key first, then the longest `*` pattern).
174
+ * Returns the relative target, `null` when the subpath is not exported, or
175
+ * `undefined` when the package has no `exports` at all.
176
+ */
177
+ export function resolveExportsSubpath(json, subpath, conditions) {
178
+ const map = exportsMap(json === null || json === void 0 ? void 0 : json.exports);
179
+ if (!map)
180
+ return undefined;
181
+ if (subpath in map)
182
+ return pickConditional(map[subpath], conditions);
183
+ let best = null;
184
+ for (const key of Object.keys(map)) {
185
+ const star = key.indexOf('*');
186
+ if (star < 0)
187
+ continue;
188
+ const prefix = key.slice(0, star);
189
+ const suffix = key.slice(star + 1);
190
+ if (subpath.startsWith(prefix) &&
191
+ subpath.endsWith(suffix) &&
192
+ subpath.length >= key.length - 1 &&
193
+ (!best || prefix.length > best.key.indexOf('*'))) {
194
+ best = {
195
+ key,
196
+ star: subpath.slice(prefix.length, subpath.length - suffix.length),
197
+ };
198
+ }
199
+ }
200
+ if (!best)
201
+ return null;
202
+ const target = pickConditional(map[best.key], conditions);
203
+ return target ? target.split('*').join(best.star) : null;
204
+ }
205
+ /** Probe a file path the way a CommonJS/ESM loader would (extension, then directory index). */
206
+ function probeFile(base) {
207
+ return __awaiter(this, void 0, void 0, function* () {
208
+ for (const candidate of [
209
+ base,
210
+ `${base}.js`,
211
+ `${base}.mjs`,
212
+ `${base}.cjs`,
213
+ path.join(base, 'index.js'),
214
+ path.join(base, 'index.mjs'),
215
+ ]) {
216
+ if (yield isFile(candidate))
217
+ return candidate;
218
+ }
219
+ return null;
220
+ });
221
+ }
222
+ /** The file a package subpath resolves to for a browser-or-anything lookup; null when none exists. */
223
+ export function resolvePackageFile(root_1, json_1, subpath_1) {
224
+ return __awaiter(this, arguments, void 0, function* (root, json, subpath, conditions = [
225
+ 'browser',
226
+ 'import',
227
+ 'module',
228
+ 'default',
229
+ 'require',
230
+ 'node',
231
+ ]) {
232
+ const viaExports = resolveExportsSubpath(json, subpath, conditions);
233
+ if (viaExports !== undefined) {
234
+ return viaExports ? probeFile(path.join(root, viaExports)) : null;
235
+ }
236
+ if (subpath === '.') {
237
+ const browser = conditions.includes('browser') &&
238
+ typeof json.browser === 'string' &&
239
+ json.browser;
240
+ const main = browser || json.module || json.main || 'index.js';
241
+ return probeFile(path.join(root, main));
242
+ }
243
+ return probeFile(path.join(root, subpath));
244
+ });
245
+ }
246
+ /** Every string target under client conditions, anywhere in `exports`. */
247
+ function clientExportTargets(json) {
248
+ const map = exportsMap(json === null || json === void 0 ? void 0 : json.exports);
249
+ if (!map) {
250
+ const main = (typeof (json === null || json === void 0 ? void 0 : json.browser) === 'string' && json.browser) ||
251
+ (json === null || json === void 0 ? void 0 : json.module) ||
252
+ (json === null || json === void 0 ? void 0 : json.main) ||
253
+ 'index.js';
254
+ return [main];
255
+ }
256
+ const out = [];
257
+ const visit = (target) => {
258
+ if (typeof target === 'string')
259
+ out.push(target);
260
+ else if (Array.isArray(target))
261
+ target.forEach(visit);
262
+ else if (target && typeof target === 'object') {
263
+ for (const [key, value] of Object.entries(target)) {
264
+ if (!NON_CLIENT_CONDITIONS.has(key))
265
+ visit(value);
266
+ }
267
+ }
268
+ };
269
+ Object.values(map).forEach(visit);
270
+ return out;
271
+ }
272
+ /** Expand one export target (possibly a `*` pattern) to the files it can reach. */
273
+ function expandTarget(root, target) {
274
+ return __awaiter(this, void 0, void 0, function* () {
275
+ const abs = path.join(root, target);
276
+ const star = abs.indexOf('*');
277
+ if (star < 0) {
278
+ const file = yield probeFile(abs);
279
+ return file ? [file] : [];
280
+ }
281
+ const prefix = abs.slice(0, star);
282
+ const suffix = abs.slice(star + 1);
283
+ const dir = prefix.endsWith(path.sep) ? prefix : path.dirname(prefix);
284
+ return listFiles(dir, (f) => f.startsWith(prefix) && f.endsWith(suffix) && !f.endsWith('.d.ts'));
285
+ });
286
+ }
287
+ let lexerReady = null;
288
+ let esbuildTransform = null;
289
+ /** The static and literal-dynamic import specifiers of a module, plus `require('x')` calls. */
290
+ export function importSpecifiers(code, file) {
291
+ return __awaiter(this, void 0, void 0, function* () {
292
+ lexerReady !== null && lexerReady !== void 0 ? lexerReady : (lexerReady = initLexer);
293
+ yield lexerReady;
294
+ let js = code;
295
+ if (/\.[mc]?tsx?$/.test(file) || /\.jsx$/.test(file)) {
296
+ esbuildTransform !== null && esbuildTransform !== void 0 ? esbuildTransform : (esbuildTransform = (yield import('esbuild')).transform);
297
+ try {
298
+ js = (yield esbuildTransform(code, {
299
+ loader: file.endsWith('x')
300
+ ? /\.tsx$/.test(file)
301
+ ? 'tsx'
302
+ : 'jsx'
303
+ : 'ts',
304
+ format: 'esm',
305
+ jsx: 'automatic',
306
+ tsconfigRaw: { compilerOptions: { experimentalDecorators: true } },
307
+ logLevel: 'silent',
308
+ })).code;
309
+ }
310
+ catch (_a) {
311
+ return [];
312
+ }
313
+ }
314
+ const out = new Set();
315
+ try {
316
+ const [imports] = parseImports(js);
317
+ for (const imp of imports)
318
+ if (imp.n)
319
+ out.add(imp.n);
320
+ }
321
+ catch (_b) {
322
+ // Not parseable as a module (JSX in a .js file, say): fall back to the textual forms.
323
+ for (const m of js.matchAll(/(?:\bfrom\s*|\bimport\s*\(?\s*)['"]([^'"\n]+)['"]/g))
324
+ out.add(m[1]);
325
+ }
326
+ for (const m of js.matchAll(/\brequire\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g))
327
+ out.add(m[1]);
328
+ return [...out];
329
+ });
330
+ }
331
+ const CACHE_VERSION = 1;
332
+ function loadCache(file) {
333
+ return __awaiter(this, void 0, void 0, function* () {
334
+ const empty = {
335
+ data: { version: CACHE_VERSION, graphs: {}, serverOnly: {} },
336
+ dirty: false,
337
+ };
338
+ if (!file)
339
+ return empty;
340
+ try {
341
+ const data = JSON.parse(yield fs.readFile(file, 'utf8'));
342
+ return (data === null || data === void 0 ? void 0 : data.version) === CACHE_VERSION && data.graphs && data.serverOnly
343
+ ? { data, dirty: false }
344
+ : empty;
345
+ }
346
+ catch (_a) {
347
+ return empty;
348
+ }
349
+ });
350
+ }
351
+ function saveCache(file, cache) {
352
+ return __awaiter(this, void 0, void 0, function* () {
353
+ if (!file || !cache.dirty)
354
+ return;
355
+ try {
356
+ yield fs.mkdir(path.dirname(file), { recursive: true });
357
+ const tmp = `${file}.${process.pid}.tmp`;
358
+ yield fs.writeFile(tmp, JSON.stringify(cache.data));
359
+ yield fs.rename(tmp, file);
360
+ }
361
+ catch (_a) {
362
+ // A cache is an optimisation: an unwritable node_modules just means a full scan next time.
363
+ }
364
+ });
365
+ }
366
+ /** Limit concurrent file reads: a few hundred open at once can exceed the fd limit. */
367
+ function semaphore(limit) {
368
+ let active = 0;
369
+ const waiting = [];
370
+ return (fn) => __awaiter(this, void 0, void 0, function* () {
371
+ var _a;
372
+ if (active >= limit)
373
+ yield new Promise((r) => waiting.push(r));
374
+ active++;
375
+ try {
376
+ return yield fn();
377
+ }
378
+ finally {
379
+ active--;
380
+ (_a = waiting.shift()) === null || _a === void 0 ? void 0 : _a();
381
+ }
382
+ });
383
+ }
384
+ /** Resolve a relative import to a module file, including the TS-source `./Sibling.js` → `.ts(x)` convention. */
385
+ function resolveRelative(importer, specifier) {
386
+ return __awaiter(this, void 0, void 0, function* () {
387
+ if (ASSET_SPECIFIER.test(specifier))
388
+ return null;
389
+ const base = path.resolve(path.dirname(importer), specifier);
390
+ const candidates = [];
391
+ if (/\.[mc]?tsx?$/.test(importer) && /\.jsx?$/.test(base)) {
392
+ candidates.push(base.replace(/\.jsx?$/, '.ts'), base.replace(/\.jsx?$/, '.tsx'));
393
+ }
394
+ candidates.push(base);
395
+ for (const ext of ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'])
396
+ candidates.push(base + ext);
397
+ for (const ext of ['.js', '.mjs', '.ts', '.tsx'])
398
+ candidates.push(path.join(base, 'index' + ext));
399
+ for (const c of candidates) {
400
+ if (SOURCE_FILE.test(c) && (yield isFile(c)))
401
+ return c;
402
+ }
403
+ return null;
404
+ });
405
+ }
406
+ const readLimit = semaphore(64);
407
+ /** Read the module graph reachable from `entries` through relative imports (and `followBare`). */
408
+ function readGraph(entries_1) {
409
+ return __awaiter(this, arguments, void 0, function* (entries, opts = {}) {
410
+ var _a;
411
+ const nodes = new Map();
412
+ const maxFiles = (_a = opts.maxFiles) !== null && _a !== void 0 ? _a : Infinity;
413
+ let stopped = false;
414
+ const visit = (file) => __awaiter(this, void 0, void 0, function* () {
415
+ var _a;
416
+ if (stopped || nodes.has(file) || nodes.size >= maxFiles)
417
+ return;
418
+ const node = { file, bare: [], rel: [] };
419
+ nodes.set(file, node);
420
+ const specs = yield readLimit(() => __awaiter(this, void 0, void 0, function* () {
421
+ try {
422
+ return yield importSpecifiers(yield fs.readFile(file, 'utf8'), file);
423
+ }
424
+ catch (_a) {
425
+ return [];
426
+ }
427
+ }));
428
+ const children = yield Promise.all(specs.map((spec) => __awaiter(this, void 0, void 0, function* () {
429
+ if (spec.startsWith('.') || spec.startsWith('/')) {
430
+ const resolved = yield resolveRelative(file, spec);
431
+ if (resolved)
432
+ node.rel.push(resolved);
433
+ return resolved;
434
+ }
435
+ node.bare.push(spec);
436
+ return opts.followBare ? opts.followBare(spec, file) : null;
437
+ })));
438
+ if ((_a = opts.stopWhen) === null || _a === void 0 ? void 0 : _a.call(opts, node)) {
439
+ stopped = true;
440
+ return;
441
+ }
442
+ yield Promise.all(children.filter((c) => !!c).map(visit));
443
+ });
444
+ yield Promise.all(entries.map(visit));
445
+ return nodes;
446
+ });
447
+ }
448
+ /** True when the package declares a browser entry anywhere (`browser` field or condition). */
449
+ function hasBrowserEntry(json) {
450
+ if (json === null || json === void 0 ? void 0 : json.browser)
451
+ return true;
452
+ let found = false;
453
+ const visit = (v) => {
454
+ if (found || !v || typeof v !== 'object')
455
+ return;
456
+ for (const [key, value] of Object.entries(v)) {
457
+ if (key === 'browser')
458
+ found = true;
459
+ else
460
+ visit(value);
461
+ }
462
+ };
463
+ visit(json === null || json === void 0 ? void 0 : json.exports);
464
+ return found;
465
+ }
466
+ const isBuiltinSpecifier = (spec) => spec.startsWith('node:') || isBuiltin(spec);
467
+ const subpathOf = (specifier) => {
468
+ const name = packageNameOf(specifier);
469
+ return specifier === name ? '.' : `.${specifier.slice(name.length)}`;
470
+ };
471
+ const NODE_CONDITIONS = ['import', 'module', 'default', 'require', 'node'];
472
+ /** `readInstalledPkg`, memoised for one scan. */
473
+ function memoInstalled() {
474
+ const cache = new Map();
475
+ return (name, fromDir) => {
476
+ const key = `${name}\0${fromDir}`;
477
+ if (!cache.has(key))
478
+ cache.set(key, readInstalledPkg(name, fromDir));
479
+ return cache.get(key);
480
+ };
481
+ }
482
+ /**
483
+ * Server-only: no browser entry, and the module graph behind the imported
484
+ * subpath — followed through its own dependencies, as long as those have no
485
+ * browser entry either — reaches a Node builtin. Such a package (express,
486
+ * sharp, bcrypt, jsonwebtoken, cookie-parser) serves a framework package's
487
+ * server code; listing it would make esbuild bundle it for the browser.
488
+ */
489
+ export function isServerOnlyDependency(root_1, json_1, subpath_1) {
490
+ return __awaiter(this, arguments, void 0, function* (root, json, subpath, lookup = readInstalledPkg) {
491
+ if (hasBrowserEntry(json))
492
+ return false;
493
+ const entry = yield resolvePackageFile(root, json, subpath, NODE_CONDITIONS);
494
+ if (!entry)
495
+ return false;
496
+ let builtin = false;
497
+ yield readGraph([entry], {
498
+ maxFiles: MAX_DEP_FILES,
499
+ followBare: (spec, importer) => __awaiter(this, void 0, void 0, function* () {
500
+ if (!isBarePackageSpecifier(spec))
501
+ return null;
502
+ const dep = yield lookup(packageNameOf(spec), path.dirname(importer));
503
+ if (!dep || hasBrowserEntry(dep.json))
504
+ return null;
505
+ return resolvePackageFile(dep.root, dep.json, subpathOf(spec), NODE_CONDITIONS);
506
+ }),
507
+ stopWhen: (node) => (builtin = builtin || node.bare.some(isBuiltinSpecifier)),
508
+ });
509
+ return builtin;
510
+ });
511
+ }
512
+ /** The files a framework package's client code can start from. */
513
+ function clientEntryFiles(pkg) {
514
+ return __awaiter(this, void 0, void 0, function* () {
515
+ if (pkg.source) {
516
+ return listFiles(path.join(pkg.root, 'src'), (f) => SOURCE_FILE.test(f) && !IGNORED_SOURCE_FILE.test(f));
517
+ }
518
+ return (yield Promise.all(clientExportTargets(pkg.json).map((t) => expandTarget(pkg.root, t)))).flat();
519
+ });
520
+ }
521
+ /**
522
+ * Scan the framework packages among `excluded` for the third-party
523
+ * dependencies their client code imports, and work out the
524
+ * `optimizeDeps.include` entry for each.
525
+ *
526
+ * Framework packages export whole directories (`"./*": "./lib/esm/*.js"`), so
527
+ * "reachable from the client exports" is every file — server code included. A
528
+ * file is therefore treated as SERVER code when it imports a Node builtin or a
529
+ * server-only dependency, or relatively imports a file that does; only the
530
+ * imports of the remaining files are listed.
531
+ */
532
+ export function scanLinkedClientDeps(excluded_1) {
533
+ return __awaiter(this, arguments, void 0, function* (excluded, opts = {}) {
534
+ var _a, _b, _c, _d, _e;
535
+ const cwd = path.resolve((_a = opts.cwd) !== null && _a !== void 0 ? _a : process.cwd());
536
+ const deny = [...DEFAULT_DENY, ...((_b = opts.deny) !== null && _b !== void 0 ? _b : [])];
537
+ const excludedSet = new Set(excluded);
538
+ const sourceByName = new Map(((_c = opts.sourceWorkspaces) !== null && _c !== void 0 ? _c : []).map((w) => [w.name, w]));
539
+ const lookup = memoInstalled();
540
+ const cache = yield loadCache(opts.cacheFile);
541
+ const unresolvedExcludes = [];
542
+ // 1. The excluded packages to scan: every source workspace (Vite does not crawl
543
+ // an excluded package, whatever it is), and the installed framework packages.
544
+ // A package with a `bin` is a command-line tool (`@_linked/cli`), whose code
545
+ // no browser loads; scanning it would only list build tooling.
546
+ const frameworkPackages = (yield Promise.all([...excludedSet].map((name) => __awaiter(this, void 0, void 0, function* () {
547
+ const ws = sourceByName.get(name);
548
+ if (ws) {
549
+ const root = yield realpath(path.dirname(ws.srcDir));
550
+ let json = {};
551
+ try {
552
+ json = JSON.parse(yield fs.readFile(path.join(root, 'package.json'), 'utf8'));
553
+ }
554
+ catch (_a) { }
555
+ return { name, root, json, source: true };
556
+ }
557
+ const installed = yield lookup(name, cwd);
558
+ if (!installed) {
559
+ unresolvedExcludes.push(name);
560
+ return null;
561
+ }
562
+ if (!isFrameworkPkg(name) && installed.json.linkedPackage !== true)
563
+ return null;
564
+ return {
565
+ name,
566
+ root: yield realpath(installed.root),
567
+ json: installed.json,
568
+ source: false,
569
+ };
570
+ })))).filter((p) => { var _a; return !!p && !((_a = p.json) === null || _a === void 0 ? void 0 : _a.bin); });
571
+ // 2. Each package's module graph.
572
+ const owner = new Map();
573
+ const nodes = new Map();
574
+ yield Promise.all(frameworkPackages.map((pkg) => __awaiter(this, void 0, void 0, function* () {
575
+ const cacheKey = pkg.source ? null : `${pkg.root}\0${pkg.json.version}`;
576
+ let graph;
577
+ if (cacheKey && cache.data.graphs[cacheKey]) {
578
+ graph = new Map(Object.entries(cache.data.graphs[cacheKey]).map(([file, n]) => [
579
+ file,
580
+ Object.assign({ file }, n),
581
+ ]));
582
+ }
583
+ else {
584
+ graph = yield readGraph(yield clientEntryFiles(pkg));
585
+ if (cacheKey) {
586
+ cache.data.graphs[cacheKey] = Object.fromEntries([...graph].map(([file, n]) => [file, { bare: n.bare, rel: n.rel }]));
587
+ cache.dirty = true;
588
+ }
589
+ }
590
+ for (const [file, node] of graph) {
591
+ if (nodes.has(file))
592
+ continue;
593
+ nodes.set(file, node);
594
+ owner.set(file, pkg);
595
+ }
596
+ })));
597
+ // 3. A verdict for every (package, dependency specifier) pair.
598
+ const verdicts = new Map();
599
+ const serverOnly = new Map();
600
+ const pairKey = (pkg, spec) => `${pkg.name}\0${spec}`;
601
+ const verdictFor = (pkg, spec) => {
602
+ const key = pairKey(pkg, spec);
603
+ if (!verdicts.has(key)) {
604
+ verdicts.set(key, (() => __awaiter(this, void 0, void 0, function* () {
605
+ if (!isBarePackageSpecifier(spec))
606
+ return null;
607
+ const depName = packageNameOf(spec);
608
+ if (depName === pkg.name || isFrameworkPkg(depName))
609
+ return null;
610
+ const installed = yield lookup(depName, pkg.root);
611
+ if (!installed)
612
+ return { reason: 'unresolved', depName };
613
+ if (installed.json.linkedPackage === true)
614
+ return null;
615
+ const depRoot = yield realpath(installed.root);
616
+ const cacheKey = `${depRoot}\0${subpathOf(spec)}`;
617
+ if (!serverOnly.has(cacheKey)) {
618
+ const persisted = `${cacheKey}\0${installed.json.version}`;
619
+ serverOnly.set(cacheKey, persisted in cache.data.serverOnly
620
+ ? Promise.resolve(cache.data.serverOnly[persisted])
621
+ : isServerOnlyDependency(depRoot, installed.json, subpathOf(spec), lookup).then((v) => {
622
+ cache.data.serverOnly[persisted] = v;
623
+ cache.dirty = true;
624
+ return v;
625
+ }));
626
+ }
627
+ return {
628
+ reason: (yield serverOnly.get(cacheKey))
629
+ ? 'server-only'
630
+ : undefined,
631
+ depName,
632
+ depRoot,
633
+ };
634
+ }))());
635
+ }
636
+ return verdicts.get(key);
637
+ };
638
+ yield Promise.all([...nodes.values()].flatMap((n) => n.bare.map((spec) => verdictFor(owner.get(n.file), spec))));
639
+ // 4. Server code: files importing a builtin or a server-only dependency, and
640
+ // every file that relatively imports one of those.
641
+ const server = new Set();
642
+ const importers = new Map();
643
+ for (const node of nodes.values()) {
644
+ for (const dep of node.rel) {
645
+ if (!importers.has(dep))
646
+ importers.set(dep, []);
647
+ importers.get(dep).push(node.file);
648
+ }
649
+ }
650
+ const queue = [];
651
+ for (const node of nodes.values()) {
652
+ const pkg = owner.get(node.file);
653
+ for (const spec of node.bare) {
654
+ if (isBuiltinSpecifier(spec) ||
655
+ ((_d = (yield verdicts.get(pairKey(pkg, spec)))) === null || _d === void 0 ? void 0 : _d.reason) === 'server-only') {
656
+ server.add(node.file);
657
+ queue.push(node.file);
658
+ break;
659
+ }
660
+ }
661
+ }
662
+ while (queue.length) {
663
+ for (const parent of (_e = importers.get(queue.pop())) !== null && _e !== void 0 ? _e : []) {
664
+ if (!server.has(parent)) {
665
+ server.add(parent);
666
+ queue.push(parent);
667
+ }
668
+ }
669
+ }
670
+ // 5. Entries from client files; everything else is reported as skipped.
671
+ const rootCopy = new Map();
672
+ const rootResolution = (depName) => {
673
+ if (!rootCopy.has(depName)) {
674
+ rootCopy.set(depName, lookup(depName, cwd).then((r) => (r ? realpath(r.root) : null)));
675
+ }
676
+ return rootCopy.get(depName);
677
+ };
678
+ const entries = new Map();
679
+ const clientPairs = new Set();
680
+ const skipped = new Map();
681
+ const skip = (specifier, from, reason) => {
682
+ skipped.set(`${specifier}\0${from}`, { specifier, from, reason });
683
+ };
684
+ for (const node of nodes.values()) {
685
+ if (server.has(node.file))
686
+ continue;
687
+ const pkg = owner.get(node.file);
688
+ for (const spec of node.bare)
689
+ clientPairs.add(pairKey(pkg, spec));
690
+ }
691
+ for (const node of nodes.values()) {
692
+ const pkg = owner.get(node.file);
693
+ for (const spec of node.bare) {
694
+ const key = pairKey(pkg, spec);
695
+ const verdict = yield verdicts.get(key);
696
+ if (!verdict)
697
+ continue;
698
+ if (verdict.reason) {
699
+ skip(spec, pkg.name, verdict.reason);
700
+ continue;
701
+ }
702
+ if (!clientPairs.has(key)) {
703
+ if (!skipped.has(`${spec}\0${pkg.name}`))
704
+ skip(spec, pkg.name, 'server-code');
705
+ continue;
706
+ }
707
+ if (excludedSet.has(verdict.depName) || excludedSet.has(spec)) {
708
+ skip(spec, pkg.name, 'excluded');
709
+ continue;
710
+ }
711
+ const nested = (yield rootResolution(verdict.depName)) !== verdict.depRoot;
712
+ const entry = nested ? `${pkg.name} > ${spec}` : spec;
713
+ if (matchesDeny(deny, entry, spec, verdict.depName)) {
714
+ skip(spec, pkg.name, 'denied');
715
+ continue;
716
+ }
717
+ if (!entries.has(entry)) {
718
+ entries.set(entry, {
719
+ entry,
720
+ specifier: spec,
721
+ from: pkg.name,
722
+ depName: verdict.depName,
723
+ depRoot: verdict.depRoot,
724
+ nested,
725
+ });
726
+ }
727
+ }
728
+ }
729
+ for (const { specifier, from } of entries.values())
730
+ skipped.delete(`${specifier}\0${from}`);
731
+ yield saveCache(opts.cacheFile, cache);
732
+ return {
733
+ entries: [...entries.values()].sort((a, b) => a.entry.localeCompare(b.entry)),
734
+ skipped: [...skipped.values()].sort((a, b) => a.specifier.localeCompare(b.specifier) || a.from.localeCompare(b.from)),
735
+ frameworkPackages,
736
+ unresolvedExcludes,
737
+ filesScanned: nodes.size,
738
+ };
739
+ });
740
+ }
741
+ /** Just the entries, for `optimizeDeps.include`. */
742
+ export function linkedClientDepIncludes(excluded_1) {
743
+ return __awaiter(this, arguments, void 0, function* (excluded, opts = {}) {
744
+ return (yield scanLinkedClientDeps(excluded, opts)).entries.map((e) => e.entry);
745
+ });
746
+ }
747
+ /**
748
+ * The most recent scan the plugin ran in this process, with the app's own
749
+ * include list — `linked doctor` loads the app's config and reads this, so it
750
+ * checks exactly what the dev server would use.
751
+ */
752
+ export const lastClientDepScan = ((_a = (_b = globalThis)[_c = Symbol.for('@_linked/cli.lastClientDepScan')]) !== null && _a !== void 0 ? _a : (_b[_c] = { current: null }));
753
+ /** Where the scan cache lives: beside Vite's own cache, inside the app's node_modules. */
754
+ export const defaultClientDepCacheFile = (cwd) => path.join(cwd, 'node_modules', '.cache', 'linked', 'client-deps.json');
755
+ const debugEnabled = () => { var _a; return /(^|[\s,])(linked(:[\w*]+)?|\*)($|[\s,])/.test((_a = process.env.DEBUG) !== null && _a !== void 0 ? _a : ''); };
756
+ /**
757
+ * Adds the scan's entries to `optimizeDeps.include` in dev.
758
+ *
759
+ * It is a `config` hook ordered `post` rather than a value computed inside
760
+ * `createViteConfig`, because the exclude list it reads is only final once the
761
+ * app has merged its own config on top — an app that excludes every installed
762
+ * `@_linked/*` package does that in its own `vite.config.ts`. Vite concatenates
763
+ * the returned array onto the app's own `include`, which is never replaced.
764
+ */
765
+ export function linkedClientDepIncludesPlugin(opts = {}) {
766
+ // What the app itself lists, before other plugins add theirs
767
+ // (@vitejs/plugin-react adds `react`, `react/jsx-runtime`…): `linked doctor`
768
+ // must not tell anyone to delete an entry they never wrote.
769
+ let appInclude = [];
770
+ const snapshot = {
771
+ name: 'linked:client-dep-includes:app-include',
772
+ apply: 'serve',
773
+ config: {
774
+ order: 'pre',
775
+ handler(userConfig) {
776
+ var _a, _b;
777
+ appInclude = [...((_b = (_a = userConfig.optimizeDeps) === null || _a === void 0 ? void 0 : _a.include) !== null && _b !== void 0 ? _b : [])];
778
+ },
779
+ },
780
+ };
781
+ return [
782
+ snapshot,
783
+ {
784
+ name: 'linked:client-dep-includes',
785
+ apply: 'serve',
786
+ config: {
787
+ order: 'post',
788
+ handler(userConfig) {
789
+ return __awaiter(this, void 0, void 0, function* () {
790
+ var _a, _b, _c, _d, _e, _f;
791
+ const optimizeDeps = (_a = userConfig.optimizeDeps) !== null && _a !== void 0 ? _a : {};
792
+ // `linked start --api-only` has no browser client to optimise for.
793
+ if (optimizeDeps.noDiscovery)
794
+ return;
795
+ const cwd = path.resolve((_b = userConfig.root) !== null && _b !== void 0 ? _b : process.cwd());
796
+ const started = performance.now();
797
+ const scan = yield scanLinkedClientDeps((_c = optimizeDeps.exclude) !== null && _c !== void 0 ? _c : [], {
798
+ cwd,
799
+ sourceWorkspaces: opts.sourceWorkspaces,
800
+ deny: opts.deny,
801
+ cacheFile: opts.cacheFile === false
802
+ ? undefined
803
+ : ((_d = opts.cacheFile) !== null && _d !== void 0 ? _d : defaultClientDepCacheFile(cwd)),
804
+ });
805
+ const existing = new Set((_e = optimizeDeps.include) !== null && _e !== void 0 ? _e : []);
806
+ const added = scan.entries
807
+ .map((e) => e.entry)
808
+ .filter((e) => !existing.has(e));
809
+ lastClientDepScan.current = {
810
+ cwd,
811
+ scan,
812
+ appInclude,
813
+ exclude: (_f = optimizeDeps.exclude) !== null && _f !== void 0 ? _f : [],
814
+ };
815
+ if (debugEnabled()) {
816
+ console.log(`[linked] optimizeDeps.include: ${added.length} entries for linked packages' client dependencies ` +
817
+ `(${Math.round(performance.now() - started)}ms, ${scan.filesScanned} files)` +
818
+ added.map((e) => `\n ${e}`).join(''));
819
+ }
820
+ return added.length ? { optimizeDeps: { include: added } } : undefined;
821
+ });
822
+ },
823
+ },
824
+ },
825
+ ];
826
+ }
827
+ //# sourceMappingURL=client-dep-includes.js.map