@_linked/server 2.16.2 → 2.17.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.
@@ -4,8 +4,21 @@
4
4
  */
5
5
  export interface LinkedDependency {
6
6
  packageName: string;
7
- /** Realpath of the package root. */
7
+ /**
8
+ * Realpath of the package root whose backend the server loads: the copy the
9
+ * app itself resolves when that copy is a linked package, otherwise the
10
+ * first copy the walk found.
11
+ */
8
12
  path: string;
13
+ /**
14
+ * Every distinct installed copy of this package in the tree (realpaths),
15
+ * in the order the walk found them. More than one means the install is not
16
+ * deduplicated; only `path`'s backend is ever loaded.
17
+ */
18
+ copies: {
19
+ path: string;
20
+ version?: string;
21
+ }[];
9
22
  }
10
23
  /**
11
24
  * A package whose backend providers the server loads at boot: one whose
@@ -40,9 +53,46 @@ export declare function readInstalledPackage(name: string, fromDir: string): {
40
53
  * filter used — so the walk reads a few dozen package.json files, not the
41
54
  * whole of node_modules. Each package's dependencies are resolved from its
42
55
  * REAL location, so a symlinked workspace or localized checkout resolves its
43
- * own dependencies the way Node will when it loads it.
56
+ * own dependencies the way Node will when it loads it — which is how a
57
+ * package installed only nested (`owl/node_modules/@_linked/rdfs`) is found.
58
+ *
59
+ * The walk is keyed by real path, not name, so every installed copy of a
60
+ * package is recorded (`copies`). The copy whose backend is loaded (`path`)
61
+ * is the one the app itself resolves, so it is the same module instance the
62
+ * app's own imports reach; when the app cannot resolve the package at all
63
+ * (it is only installed nested) it is the first copy found.
44
64
  */
45
65
  export declare function discoverLinkedDependencies(appDir: string, appPackageJson: any): LinkedDependency[];
66
+ /**
67
+ * The conditions a backend entry is resolved with on the Node path. These are
68
+ * the ones Node's ESM loader applies to `import()` — so the file found is the
69
+ * file a bare `import('<pkg>/backend')` from the app would load, and Node's
70
+ * module cache (keyed by real file URL) hands both the same instance.
71
+ * `development` is deliberately absent: it is a Vite/bundler condition, and a
72
+ * production Node runtime never applies it.
73
+ */
74
+ export declare const NODE_IMPORT_CONDITIONS: readonly ["import", "node", "default"];
75
+ /**
76
+ * The absolute path of a package's backend entry file, or null when the
77
+ * package has no backend entry or the file it names does not exist.
78
+ *
79
+ * With `exports`, this follows Node's PACKAGE_EXPORTS_RESOLVE for the
80
+ * `./backend` subpath: an exact `./backend` key wins; otherwise the `./*`-style
81
+ * pattern with the longest matching prefix; condition objects (nested or not)
82
+ * pick the first key that is one of `conditions` or `default`; arrays take the
83
+ * first target that resolves. A package that has `exports` but nothing
84
+ * matching `./backend` has no backend — exactly as Node would refuse to
85
+ * import it.
86
+ *
87
+ * Without `exports`, the legacy layout: `<root>/backend.js`,
88
+ * `<root>/backend/index.js`, and the layout linked packages build to — a
89
+ * `backend.js` next to `main` (`lib/esm/backend.js`).
90
+ *
91
+ * Hand-written rather than via `resolve.exports`: that package also stops at
92
+ * a path and leaves the existence check to the caller, the subset needed is
93
+ * ~50 lines, and the server keeps one less dependency in every app's install.
94
+ */
95
+ export declare function resolveBackendEntry(pkgRoot: string, pkgJson: any, conditions?: readonly string[]): string | null;
46
96
  /**
47
97
  * Whether the dev SSR runner BUNDLES `pkgName` (it is in Vite's
48
98
  * `ssr.noExternal`) rather than handing it to Node.
@@ -52,37 +52,207 @@ export function readInstalledPackage(name, fromDir) {
52
52
  * filter used — so the walk reads a few dozen package.json files, not the
53
53
  * whole of node_modules. Each package's dependencies are resolved from its
54
54
  * REAL location, so a symlinked workspace or localized checkout resolves its
55
- * own dependencies the way Node will when it loads it.
55
+ * own dependencies the way Node will when it loads it — which is how a
56
+ * package installed only nested (`owl/node_modules/@_linked/rdfs`) is found.
57
+ *
58
+ * The walk is keyed by real path, not name, so every installed copy of a
59
+ * package is recorded (`copies`). The copy whose backend is loaded (`path`)
60
+ * is the one the app itself resolves, so it is the same module instance the
61
+ * app's own imports reach; when the app cannot resolve the package at all
62
+ * (it is only installed nested) it is the first copy found.
56
63
  */
57
64
  export function discoverLinkedDependencies(appDir, appPackageJson) {
58
65
  var _a;
59
66
  const out = [];
60
- const seen = new Set();
61
- if (appPackageJson === null || appPackageJson === void 0 ? void 0 : appPackageJson.name)
62
- seen.add(appPackageJson.name);
67
+ const byName = new Map();
68
+ const visitedRoots = new Set();
69
+ const appName = appPackageJson === null || appPackageJson === void 0 ? void 0 : appPackageJson.name;
63
70
  const visit = (name, fromDir) => {
64
71
  var _a;
65
- if (seen.has(name))
72
+ if (name === appName)
66
73
  return;
67
- seen.add(name);
68
74
  const installed = readInstalledPackage(name, fromDir);
69
75
  if (!installed || !isLinkedPackageJson(installed.json))
70
76
  return;
71
- let realRoot = installed.root;
72
- try {
73
- realRoot = fs.realpathSync(installed.root);
77
+ const realRoot = realpathOr(installed.root);
78
+ if (visitedRoots.has(realRoot))
79
+ return;
80
+ visitedRoots.add(realRoot);
81
+ let entry = byName.get(name);
82
+ if (!entry) {
83
+ entry = { packageName: name, path: realRoot, copies: [] };
84
+ byName.set(name, entry);
74
85
  }
75
- catch (_b) { }
86
+ entry.copies.push({ path: realRoot, version: installed.json.version });
76
87
  for (const dep of Object.keys((_a = installed.json.dependencies) !== null && _a !== void 0 ? _a : {})) {
77
88
  visit(dep, realRoot);
78
89
  }
79
- out.push({ packageName: name, path: realRoot });
90
+ // Pushed after its dependencies, once per name: the first copy found
91
+ // fixes the package's place in the order.
92
+ if (!out.includes(entry))
93
+ out.push(entry);
80
94
  };
81
95
  for (const name of Object.keys((_a = appPackageJson === null || appPackageJson === void 0 ? void 0 : appPackageJson.dependencies) !== null && _a !== void 0 ? _a : {})) {
82
96
  visit(name, appDir);
83
97
  }
98
+ for (const entry of out) {
99
+ if (entry.copies.length < 2)
100
+ continue;
101
+ const fromApp = readInstalledPackage(entry.packageName, appDir);
102
+ if (fromApp && isLinkedPackageJson(fromApp.json)) {
103
+ const appCopy = realpathOr(fromApp.root);
104
+ if (entry.copies.some((c) => c.path === appCopy))
105
+ entry.path = appCopy;
106
+ }
107
+ }
84
108
  return out;
85
109
  }
110
+ function realpathOr(p) {
111
+ try {
112
+ return fs.realpathSync(p);
113
+ }
114
+ catch (_a) {
115
+ return p;
116
+ }
117
+ }
118
+ /**
119
+ * The conditions a backend entry is resolved with on the Node path. These are
120
+ * the ones Node's ESM loader applies to `import()` — so the file found is the
121
+ * file a bare `import('<pkg>/backend')` from the app would load, and Node's
122
+ * module cache (keyed by real file URL) hands both the same instance.
123
+ * `development` is deliberately absent: it is a Vite/bundler condition, and a
124
+ * production Node runtime never applies it.
125
+ */
126
+ export const NODE_IMPORT_CONDITIONS = ['import', 'node', 'default'];
127
+ /**
128
+ * The absolute path of a package's backend entry file, or null when the
129
+ * package has no backend entry or the file it names does not exist.
130
+ *
131
+ * With `exports`, this follows Node's PACKAGE_EXPORTS_RESOLVE for the
132
+ * `./backend` subpath: an exact `./backend` key wins; otherwise the `./*`-style
133
+ * pattern with the longest matching prefix; condition objects (nested or not)
134
+ * pick the first key that is one of `conditions` or `default`; arrays take the
135
+ * first target that resolves. A package that has `exports` but nothing
136
+ * matching `./backend` has no backend — exactly as Node would refuse to
137
+ * import it.
138
+ *
139
+ * Without `exports`, the legacy layout: `<root>/backend.js`,
140
+ * `<root>/backend/index.js`, and the layout linked packages build to — a
141
+ * `backend.js` next to `main` (`lib/esm/backend.js`).
142
+ *
143
+ * Hand-written rather than via `resolve.exports`: that package also stops at
144
+ * a path and leaves the existence check to the caller, the subset needed is
145
+ * ~50 lines, and the server keeps one less dependency in every app's install.
146
+ */
147
+ export function resolveBackendEntry(pkgRoot, pkgJson, conditions = NODE_IMPORT_CONDITIONS) {
148
+ const exists = (rel) => {
149
+ const file = path.resolve(pkgRoot, rel);
150
+ // Never let an export target escape the package.
151
+ if (!file.startsWith(path.resolve(pkgRoot) + path.sep))
152
+ return null;
153
+ try {
154
+ return fs.statSync(file).isFile() ? file : null;
155
+ }
156
+ catch (_a) {
157
+ return null;
158
+ }
159
+ };
160
+ const exportsField = pkgJson === null || pkgJson === void 0 ? void 0 : pkgJson.exports;
161
+ if (exportsField === undefined || exportsField === null) {
162
+ const candidates = ['backend.js', 'backend/index.js'];
163
+ if (typeof (pkgJson === null || pkgJson === void 0 ? void 0 : pkgJson.main) === 'string') {
164
+ candidates.push(path.join(path.dirname(pkgJson.main), 'backend.js'));
165
+ }
166
+ candidates.push('lib/esm/backend.js');
167
+ for (const rel of candidates) {
168
+ const file = exists(rel);
169
+ if (file)
170
+ return file;
171
+ }
172
+ return null;
173
+ }
174
+ // `"exports": "./x.js"`, an array, or a condition object at the top level
175
+ // are all sugar for the "." subpath only.
176
+ if (typeof exportsField !== 'object' ||
177
+ Array.isArray(exportsField) ||
178
+ !Object.keys(exportsField).some((k) => k.startsWith('.'))) {
179
+ return null;
180
+ }
181
+ const subpath = './backend';
182
+ let target;
183
+ let patternMatch = null;
184
+ if (Object.prototype.hasOwnProperty.call(exportsField, subpath)) {
185
+ target = exportsField[subpath];
186
+ }
187
+ else {
188
+ let bestKey = null;
189
+ for (const key of Object.keys(exportsField)) {
190
+ const star = key.indexOf('*');
191
+ if (star === -1 || key.indexOf('*', star + 1) !== -1)
192
+ continue;
193
+ const prefix = key.slice(0, star);
194
+ const suffix = key.slice(star + 1);
195
+ if (subpath !== prefix &&
196
+ subpath.length >= key.length &&
197
+ subpath.startsWith(prefix) &&
198
+ subpath.endsWith(suffix) &&
199
+ (bestKey === null || patternKeyCompare(bestKey, key) > 0)) {
200
+ bestKey = key;
201
+ patternMatch = subpath.slice(prefix.length, subpath.length - suffix.length);
202
+ }
203
+ }
204
+ if (bestKey === null)
205
+ return null;
206
+ target = exportsField[bestKey];
207
+ }
208
+ const resolveTarget = (t) => {
209
+ if (typeof t === 'string') {
210
+ if (!t.startsWith('./'))
211
+ return null;
212
+ const rel = patternMatch === null ? t : t.split('*').join(patternMatch);
213
+ return exists(rel);
214
+ }
215
+ if (Array.isArray(t)) {
216
+ for (const item of t) {
217
+ const file = resolveTarget(item);
218
+ if (file)
219
+ return file;
220
+ }
221
+ return null;
222
+ }
223
+ if (t && typeof t === 'object') {
224
+ for (const key of Object.keys(t)) {
225
+ if (key === 'default' || conditions.includes(key)) {
226
+ // Node takes the first matching condition even if its target fails
227
+ // to resolve; it does not fall through to later keys.
228
+ return resolveTarget(t[key]);
229
+ }
230
+ }
231
+ }
232
+ return null;
233
+ };
234
+ return resolveTarget(target);
235
+ }
236
+ /** Node's PATTERN_KEY_COMPARE: < 0 means `a` is the more specific pattern. */
237
+ function patternKeyCompare(a, b) {
238
+ const aBase = a.indexOf('*');
239
+ const bBase = b.indexOf('*');
240
+ const baseA = aBase === -1 ? a.length : aBase + 1;
241
+ const baseB = bBase === -1 ? b.length : bBase + 1;
242
+ if (baseA > baseB)
243
+ return -1;
244
+ if (baseB > baseA)
245
+ return 1;
246
+ if (aBase === -1)
247
+ return 1;
248
+ if (bBase === -1)
249
+ return -1;
250
+ if (a.length > b.length)
251
+ return -1;
252
+ if (b.length > a.length)
253
+ return 1;
254
+ return 0;
255
+ }
86
256
  /**
87
257
  * Whether the dev SSR runner BUNDLES `pkgName` (it is in Vite's
88
258
  * `ssr.noExternal`) rather than handing it to Node.
@@ -1 +1 @@
1
- {"version":3,"file":"linkedDependencies.js","sourceRoot":"","sources":["../../../src/utils/linkedDependencies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAY7B;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAS;IAC3C,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACzD,OAAO,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC;AACrC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAY,EACZ,OAAe;IAEf,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAChC,OAAO,IAAI,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC;QAClD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;QACpD,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC/B,IAAI,CAAC;gBACH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;YAC3E,CAAC;YAAC,WAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAChC,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,0BAA0B,CACxC,MAAc,EACd,cAAmB;;IAEnB,MAAM,GAAG,GAAuB,EAAE,CAAC;IACnC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,IAAI,cAAc,aAAd,cAAc,uBAAd,cAAc,CAAE,IAAI;QAAE,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;IAExD,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,OAAe,EAAE,EAAE;;QAC9C,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO;QAC3B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,MAAM,SAAS,GAAG,oBAAoB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACtD,IAAI,CAAC,SAAS,IAAI,CAAC,mBAAmB,CAAC,SAAS,CAAC,IAAI,CAAC;YAAE,OAAO;QAC/D,IAAI,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC;QAC9B,IAAI,CAAC;YACH,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC7C,CAAC;QAAC,WAAM,CAAC,CAAA,CAAC;QACV,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAA,SAAS,CAAC,IAAI,CAAC,YAAY,mCAAI,EAAE,CAAC,EAAE,CAAC;YACjE,KAAK,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACvB,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IAClD,CAAC,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAA,cAAc,aAAd,cAAc,uBAAd,cAAc,CAAE,YAAY,mCAAI,EAAE,CAAC,EAAE,CAAC;QACnE,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,UAAmB;IACjE,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC;QACvC,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,UAAU;YACV,CAAC,CAAC,CAAC,UAAU,CAAC;YACd,CAAC,CAAC,EAAE,CAAC;IACT,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAC5B,OAAO,KAAK,KAAK,QAAQ;QACvB,CAAC,CAAC,KAAK,KAAK,OAAO;QACnB,CAAC,CAAC,KAAK,YAAY,MAAM;YACvB,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC;YACrB,CAAC,CAAC,KAAK,CACZ,CAAC;AACJ,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,eAAe,CAAC,IAAS;;IACvC,OAAO,CACL,MAAA,MAAA,MAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,MAAM,0CAAE,GAAG,0CAAE,UAAU,mCAC7B,MAAA,MAAA,MAAA,MAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,MAAM,0CAAE,YAAY,0CAAE,GAAG,0CAAE,OAAO,0CAAE,UAAU,CACrD,CAAC;AACJ,CAAC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\n/**\n * A linked package found in the app's dependency tree, wherever it is\n * installed: a workspace member, a localized checkout, or a registry install.\n */\nexport interface LinkedDependency {\n packageName: string;\n /** Realpath of the package root. */\n path: string;\n}\n\n/**\n * A package whose backend providers the server loads at boot: one whose\n * package.json declares `\"linkedPackage\": true`. That flag is the only test —\n * not the package's scope. `@_linked/localize` is in the `@_linked` scope and\n * is a CLI tool, not a linked package; a custom-scope package with the flag is\n * a linked package. The CLI's workspace discovery uses the same flag.\n *\n * The legacy `\"lincd\": true` flag is deliberately NOT enough. Those packages\n * import the old `lincd` core, and loading them at boot is what produced\n * \"Multiple versions of LINCD are loaded\" before boot loading was narrowed to\n * workspace members.\n */\nexport function isLinkedPackageJson(json: any): boolean {\n if (!json || typeof json.name !== 'string') return false;\n return json.linkedPackage === true;\n}\n\n/**\n * Find an installed package the way Node does: `<dir>/node_modules/<name>`,\n * then each parent directory's `node_modules`. Returns null when it is not\n * installed (an optional dependency, say) rather than throwing.\n */\nexport function readInstalledPackage(\n name: string,\n fromDir: string\n): { root: string; json: any } | null {\n let dir = path.resolve(fromDir);\n while (true) {\n const root = path.join(dir, 'node_modules', name);\n const pkgJsonPath = path.join(root, 'package.json');\n if (fs.existsSync(pkgJsonPath)) {\n try {\n return { root, json: JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) };\n } catch {\n return null;\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/**\n * Every linked package reachable from the app's `dependencies`, each one AFTER\n * the linked packages it depends on. Providers register middleware in the\n * order they are indexed, so a package's middleware must not run before that\n * of a package it builds on (e.g. anything reading `request.linkedAuth` after\n * @_linked/auth's JWT middleware).\n *\n * Only linked packages are recursed into — the same rule the workspace-only\n * filter used — so the walk reads a few dozen package.json files, not the\n * whole of node_modules. Each package's dependencies are resolved from its\n * REAL location, so a symlinked workspace or localized checkout resolves its\n * own dependencies the way Node will when it loads it.\n */\nexport function discoverLinkedDependencies(\n appDir: string,\n appPackageJson: any\n): LinkedDependency[] {\n const out: LinkedDependency[] = [];\n const seen = new Set<string>();\n if (appPackageJson?.name) seen.add(appPackageJson.name);\n\n const visit = (name: string, fromDir: string) => {\n if (seen.has(name)) return;\n seen.add(name);\n const installed = readInstalledPackage(name, fromDir);\n if (!installed || !isLinkedPackageJson(installed.json)) return;\n let realRoot = installed.root;\n try {\n realRoot = fs.realpathSync(installed.root);\n } catch {}\n for (const dep of Object.keys(installed.json.dependencies ?? {})) {\n visit(dep, realRoot);\n }\n out.push({ packageName: name, path: realRoot });\n };\n\n for (const name of Object.keys(appPackageJson?.dependencies ?? {})) {\n visit(name, appDir);\n }\n return out;\n}\n\n/**\n * Whether the dev SSR runner BUNDLES `pkgName` (it is in Vite's\n * `ssr.noExternal`) rather than handing it to Node.\n *\n * This decides which loader a package's backend must be loaded with. A module\n * has to be evaluated by the same loader that evaluates the package everywhere\n * else in the SSR graph, or it runs twice:\n *\n * - `vite.ssrLoadModule('<external pkg>/backend')` transforms that entry and\n * its relative imports even though the package is external, while the app's\n * bare imports of the same package are evaluated by Node — two copies of the\n * package's module state.\n * - a plain `import()` of a BUNDLED package goes to Node, while the app's\n * imports of it are evaluated by Vite — the same split the other way round.\n *\n * Vite matches `noExternal` entries against the bare package name, so a\n * subpath (`pkg/backend`) is covered by an entry for `pkg`.\n */\nexport function isBundledBySsr(pkgName: string, noExternal: unknown): boolean {\n if (noExternal === true) return true;\n const entries = Array.isArray(noExternal)\n ? noExternal\n : noExternal\n ? [noExternal]\n : [];\n return entries.some((entry) =>\n typeof entry === 'string'\n ? entry === pkgName\n : entry instanceof RegExp\n ? entry.test(pkgName)\n : false\n );\n}\n\n/** The dev SSR runner's resolved `noExternal`, wherever this Vite keeps it. */\nexport function ssrNoExternalOf(vite: any): unknown {\n return (\n vite?.config?.ssr?.noExternal ??\n vite?.config?.environments?.ssr?.resolve?.noExternal\n );\n}\n"]}
1
+ {"version":3,"file":"linkedDependencies.js","sourceRoot":"","sources":["../../../src/utils/linkedDependencies.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\n/**\n * A linked package found in the app's dependency tree, wherever it is\n * installed: a workspace member, a localized checkout, or a registry install.\n */\nexport interface LinkedDependency {\n packageName: string;\n /**\n * Realpath of the package root whose backend the server loads: the copy the\n * app itself resolves when that copy is a linked package, otherwise the\n * first copy the walk found.\n */\n path: string;\n /**\n * Every distinct installed copy of this package in the tree (realpaths),\n * in the order the walk found them. More than one means the install is not\n * deduplicated; only `path`'s backend is ever loaded.\n */\n copies: { path: string; version?: string }[];\n}\n\n/**\n * A package whose backend providers the server loads at boot: one whose\n * package.json declares `\"linkedPackage\": true`. That flag is the only test —\n * not the package's scope. `@_linked/localize` is in the `@_linked` scope and\n * is a CLI tool, not a linked package; a custom-scope package with the flag is\n * a linked package. The CLI's workspace discovery uses the same flag.\n *\n * The legacy `\"lincd\": true` flag is deliberately NOT enough. Those packages\n * import the old `lincd` core, and loading them at boot is what produced\n * \"Multiple versions of LINCD are loaded\" before boot loading was narrowed to\n * workspace members.\n */\nexport function isLinkedPackageJson(json: any): boolean {\n if (!json || typeof json.name !== 'string') return false;\n return json.linkedPackage === true;\n}\n\n/**\n * Find an installed package the way Node does: `<dir>/node_modules/<name>`,\n * then each parent directory's `node_modules`. Returns null when it is not\n * installed (an optional dependency, say) rather than throwing.\n */\nexport function readInstalledPackage(\n name: string,\n fromDir: string\n): { root: string; json: any } | null {\n let dir = path.resolve(fromDir);\n while (true) {\n const root = path.join(dir, 'node_modules', name);\n const pkgJsonPath = path.join(root, 'package.json');\n if (fs.existsSync(pkgJsonPath)) {\n try {\n return { root, json: JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) };\n } catch {\n return null;\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n/**\n * Every linked package reachable from the app's `dependencies`, each one AFTER\n * the linked packages it depends on. Providers register middleware in the\n * order they are indexed, so a package's middleware must not run before that\n * of a package it builds on (e.g. anything reading `request.linkedAuth` after\n * @_linked/auth's JWT middleware).\n *\n * Only linked packages are recursed into — the same rule the workspace-only\n * filter used — so the walk reads a few dozen package.json files, not the\n * whole of node_modules. Each package's dependencies are resolved from its\n * REAL location, so a symlinked workspace or localized checkout resolves its\n * own dependencies the way Node will when it loads it — which is how a\n * package installed only nested (`owl/node_modules/@_linked/rdfs`) is found.\n *\n * The walk is keyed by real path, not name, so every installed copy of a\n * package is recorded (`copies`). The copy whose backend is loaded (`path`)\n * is the one the app itself resolves, so it is the same module instance the\n * app's own imports reach; when the app cannot resolve the package at all\n * (it is only installed nested) it is the first copy found.\n */\nexport function discoverLinkedDependencies(\n appDir: string,\n appPackageJson: any\n): LinkedDependency[] {\n const out: LinkedDependency[] = [];\n const byName = new Map<string, LinkedDependency>();\n const visitedRoots = new Set<string>();\n const appName = appPackageJson?.name;\n\n const visit = (name: string, fromDir: string) => {\n if (name === appName) return;\n const installed = readInstalledPackage(name, fromDir);\n if (!installed || !isLinkedPackageJson(installed.json)) return;\n const realRoot = realpathOr(installed.root);\n if (visitedRoots.has(realRoot)) return;\n visitedRoots.add(realRoot);\n let entry = byName.get(name);\n if (!entry) {\n entry = { packageName: name, path: realRoot, copies: [] };\n byName.set(name, entry);\n }\n entry.copies.push({ path: realRoot, version: installed.json.version });\n for (const dep of Object.keys(installed.json.dependencies ?? {})) {\n visit(dep, realRoot);\n }\n // Pushed after its dependencies, once per name: the first copy found\n // fixes the package's place in the order.\n if (!out.includes(entry)) out.push(entry);\n };\n\n for (const name of Object.keys(appPackageJson?.dependencies ?? {})) {\n visit(name, appDir);\n }\n\n for (const entry of out) {\n if (entry.copies.length < 2) continue;\n const fromApp = readInstalledPackage(entry.packageName, appDir);\n if (fromApp && isLinkedPackageJson(fromApp.json)) {\n const appCopy = realpathOr(fromApp.root);\n if (entry.copies.some((c) => c.path === appCopy)) entry.path = appCopy;\n }\n }\n return out;\n}\n\nfunction realpathOr(p: string): string {\n try {\n return fs.realpathSync(p);\n } catch {\n return p;\n }\n}\n\n/**\n * The conditions a backend entry is resolved with on the Node path. These are\n * the ones Node's ESM loader applies to `import()` — so the file found is the\n * file a bare `import('<pkg>/backend')` from the app would load, and Node's\n * module cache (keyed by real file URL) hands both the same instance.\n * `development` is deliberately absent: it is a Vite/bundler condition, and a\n * production Node runtime never applies it.\n */\nexport const NODE_IMPORT_CONDITIONS = ['import', 'node', 'default'] as const;\n\n/**\n * The absolute path of a package's backend entry file, or null when the\n * package has no backend entry or the file it names does not exist.\n *\n * With `exports`, this follows Node's PACKAGE_EXPORTS_RESOLVE for the\n * `./backend` subpath: an exact `./backend` key wins; otherwise the `./*`-style\n * pattern with the longest matching prefix; condition objects (nested or not)\n * pick the first key that is one of `conditions` or `default`; arrays take the\n * first target that resolves. A package that has `exports` but nothing\n * matching `./backend` has no backend — exactly as Node would refuse to\n * import it.\n *\n * Without `exports`, the legacy layout: `<root>/backend.js`,\n * `<root>/backend/index.js`, and the layout linked packages build to — a\n * `backend.js` next to `main` (`lib/esm/backend.js`).\n *\n * Hand-written rather than via `resolve.exports`: that package also stops at\n * a path and leaves the existence check to the caller, the subset needed is\n * ~50 lines, and the server keeps one less dependency in every app's install.\n */\nexport function resolveBackendEntry(\n pkgRoot: string,\n pkgJson: any,\n conditions: readonly string[] = NODE_IMPORT_CONDITIONS\n): string | null {\n const exists = (rel: string) => {\n const file = path.resolve(pkgRoot, rel);\n // Never let an export target escape the package.\n if (!file.startsWith(path.resolve(pkgRoot) + path.sep)) return null;\n try {\n return fs.statSync(file).isFile() ? file : null;\n } catch {\n return null;\n }\n };\n\n const exportsField = pkgJson?.exports;\n if (exportsField === undefined || exportsField === null) {\n const candidates = ['backend.js', 'backend/index.js'];\n if (typeof pkgJson?.main === 'string') {\n candidates.push(path.join(path.dirname(pkgJson.main), 'backend.js'));\n }\n candidates.push('lib/esm/backend.js');\n for (const rel of candidates) {\n const file = exists(rel);\n if (file) return file;\n }\n return null;\n }\n\n // `\"exports\": \"./x.js\"`, an array, or a condition object at the top level\n // are all sugar for the \".\" subpath only.\n if (\n typeof exportsField !== 'object' ||\n Array.isArray(exportsField) ||\n !Object.keys(exportsField).some((k) => k.startsWith('.'))\n ) {\n return null;\n }\n\n const subpath = './backend';\n let target: any;\n let patternMatch: string | null = null;\n if (Object.prototype.hasOwnProperty.call(exportsField, subpath)) {\n target = exportsField[subpath];\n } else {\n let bestKey: string | null = null;\n for (const key of Object.keys(exportsField)) {\n const star = key.indexOf('*');\n if (star === -1 || key.indexOf('*', star + 1) !== -1) continue;\n const prefix = key.slice(0, star);\n const suffix = key.slice(star + 1);\n if (\n subpath !== prefix &&\n subpath.length >= key.length &&\n subpath.startsWith(prefix) &&\n subpath.endsWith(suffix) &&\n (bestKey === null || patternKeyCompare(bestKey, key) > 0)\n ) {\n bestKey = key;\n patternMatch = subpath.slice(prefix.length, subpath.length - suffix.length);\n }\n }\n if (bestKey === null) return null;\n target = exportsField[bestKey];\n }\n\n const resolveTarget = (t: any): string | null => {\n if (typeof t === 'string') {\n if (!t.startsWith('./')) return null;\n const rel = patternMatch === null ? t : t.split('*').join(patternMatch);\n return exists(rel);\n }\n if (Array.isArray(t)) {\n for (const item of t) {\n const file = resolveTarget(item);\n if (file) return file;\n }\n return null;\n }\n if (t && typeof t === 'object') {\n for (const key of Object.keys(t)) {\n if (key === 'default' || conditions.includes(key)) {\n // Node takes the first matching condition even if its target fails\n // to resolve; it does not fall through to later keys.\n return resolveTarget(t[key]);\n }\n }\n }\n return null;\n };\n return resolveTarget(target);\n}\n\n/** Node's PATTERN_KEY_COMPARE: < 0 means `a` is the more specific pattern. */\nfunction patternKeyCompare(a: string, b: string): number {\n const aBase = a.indexOf('*');\n const bBase = b.indexOf('*');\n const baseA = aBase === -1 ? a.length : aBase + 1;\n const baseB = bBase === -1 ? b.length : bBase + 1;\n if (baseA > baseB) return -1;\n if (baseB > baseA) return 1;\n if (aBase === -1) return 1;\n if (bBase === -1) return -1;\n if (a.length > b.length) return -1;\n if (b.length > a.length) return 1;\n return 0;\n}\n\n/**\n * Whether the dev SSR runner BUNDLES `pkgName` (it is in Vite's\n * `ssr.noExternal`) rather than handing it to Node.\n *\n * This decides which loader a package's backend must be loaded with. A module\n * has to be evaluated by the same loader that evaluates the package everywhere\n * else in the SSR graph, or it runs twice:\n *\n * - `vite.ssrLoadModule('<external pkg>/backend')` transforms that entry and\n * its relative imports even though the package is external, while the app's\n * bare imports of the same package are evaluated by Node — two copies of the\n * package's module state.\n * - a plain `import()` of a BUNDLED package goes to Node, while the app's\n * imports of it are evaluated by Vite — the same split the other way round.\n *\n * Vite matches `noExternal` entries against the bare package name, so a\n * subpath (`pkg/backend`) is covered by an entry for `pkg`.\n */\nexport function isBundledBySsr(pkgName: string, noExternal: unknown): boolean {\n if (noExternal === true) return true;\n const entries = Array.isArray(noExternal)\n ? noExternal\n : noExternal\n ? [noExternal]\n : [];\n return entries.some((entry) =>\n typeof entry === 'string'\n ? entry === pkgName\n : entry instanceof RegExp\n ? entry.test(pkgName)\n : false\n );\n}\n\n/** The dev SSR runner's resolved `noExternal`, wherever this Vite keeps it. */\nexport function ssrNoExternalOf(vite: any): unknown {\n return (\n vite?.config?.ssr?.noExternal ??\n vite?.config?.environments?.ssr?.resolve?.noExternal\n );\n}\n"]}
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/linked-fw/server.git"
12
12
  },
13
- "version": "2.16.2",
13
+ "version": "2.17.0",
14
14
  "packageManager": "npm@11.20.0",
15
15
  "linkedPackage": true,
16
16
  "type": "module",
package/readme.md CHANGED
@@ -250,7 +250,7 @@ With `apiOnly` the server skips page rendering. It installs no SPA catch-all, so
250
250
 
251
251
  Every provider — generic (`BackendProvider`) or shape-scoped (`ShapeProvider`) — has a lifecycle the framework drives:
252
252
 
253
- 1. **Construction** — `LincdServer.indexPackageBackendProviders(pkg)` reads `${pkg}/backend`, finds every exported provider class, calls `new providerClass(server, lincdServer)` for each.
253
+ 1. **Construction** — `LincdServer.indexPackageBackendProviders(pkg)` loads the package's `./backend` entry, finds every exported provider class, calls `new providerClass(server, lincdServer)` for each. The entry is resolved from the directory where the package is actually installed — including a copy nested in another package's `node_modules` — through its `exports` map (`./backend`, or a `./*` pattern, with the `import`/`node`/`default` conditions), and imported by file URL. A package whose exports map to no existing file has no backend and is skipped quietly; a backend file that exists but fails to load is logged as an error naming the package and the file. If a linked package is installed more than once, only the copy the app itself resolves is loaded, with a warning to run `npm dedupe`.
254
254
  2. **Boot hooks** — `setupBeforeControllers()`, `setupBeforeCatchAllControllers()`, `setupAfterControllers()` run in that order during server startup.
255
255
  3. **Per-request** — `initRequest(req, res)` then `supplyDataForRequest(req, res, data)` on every incoming request.
256
256
  4. **Dispose** — `dispose()` runs when the provider is being torn down. In dev mode this happens on HMR (a watched source file in the same package changed) and on graceful shutdown. In production it only runs on shutdown.