@_linked/server 2.15.0 → 2.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @\_linked/server
2
2
 
3
+ ## 2.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#119](https://github.com/linked-fw/server/pull/119) [`04d3708`](https://github.com/linked-fw/server/commit/04d3708c41a2b15a032b1b2cfa0bf9687a2d26bf) Thanks [@renovate](https://github.com/apps/renovate)! - Drop the unused direct `body-parser` dependency. The server has always parsed request bodies with Express 4's built-in `express.json({ limit: '50mb' })` (body-parser 1.x bundled inside Express), so nothing changes at runtime. The parser now lives in `createJsonBodyParser()` and its behaviour — `/call` arguments, the 50mb limit, 413/400 errors, multipart and urlencoded bodies left unparsed — is covered by tests.
8
+
9
+ ## 2.16.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#122](https://github.com/linked-fw/server/pull/122) [`b6c2c9a`](https://github.com/linked-fw/server/commit/b6c2c9ac689c614966f97f39cf18c0bab7683e16) Thanks [@flyon](https://github.com/flyon)! - Load the backend providers of installed linked packages at boot, not only those of local workspace packages.
14
+
15
+ Every package in the app's dependency tree whose package.json declares `"linkedPackage": true` — registry install, localized checkout or workspace member — now has its `/backend` providers indexed at startup, each after the linked packages it depends on. Before, a registry-installed package's provider was indexed only on the first `/call/<pkg>/...`, so its `initRequest` / `supplyDataForRequest` hooks were silent until then and its `setupBeforeControllers` / `setupAfterControllers` never ran at all. With `@_linked/auth` installed from the registry, that meant a signed-in SSR page after a restart shipped an empty request-json and failed to hydrate.
16
+
17
+ The `"linkedPackage": true` flag is the only test, whatever the package's scope: an `@_linked/*` package without it (such as the `@_linked/localize` CLI tool) is not loaded, and a package in any other scope with it is. Legacy `"lincd": true` packages are still not loaded at boot.
18
+
19
+ Lazy indexing on `/call` stays as a fallback and shares one in-flight indexing per package with boot, so a provider is never constructed twice.
20
+
21
+ A provider whose hook throws no longer takes the server down with it. Boot lifecycle hooks (`setupBeforeControllers`, `setupBeforeCatchAllControllers`, `setupAfterControllers`) and per-request hooks (`initRequest`, `supplyDataForRequest`) now run per provider: an error is logged as `[linked] <package> <hook> failed: …` and the remaining providers still run. Before, a throwing boot hook aborted startup, and a synchronous throw in `supplyDataForRequest` failed every page request without naming the provider. This applies to local workspace providers as well as installed ones.
22
+
23
+ In dev, a package's backend is now loaded by the loader that owns the package: `vite.ssrLoadModule` for what the SSR runner bundles (`ssr.noExternal` — workspaces and `SSR_ENTRY_PACKAGES`), Node's `import()` for what it leaves external. Loading an external package's `/backend` through `ssrLoadModule` evaluated that package a second time, apart from the copy the app's own imports use.
24
+
25
+ Apps should be on `@_linked/sentry` 1.1.3 or later: before that, its `setupBeforeControllers` called `require` in ESM and threw `ReferenceError: require is not defined` whenever `SENTRY_DSN` and `SITE_ROOT` are set outside development. Now that that hook runs at boot, an older version logs the error instead of aborting startup, but Sentry stays inactive.
26
+
3
27
  ## 2.15.0
4
28
 
5
29
  ### Minor Changes
@@ -38,6 +38,7 @@ export declare class LinkedServer extends Shape {
38
38
  private analyse;
39
39
  private shapeProviders;
40
40
  private genericProviders;
41
+ private providerIndexing;
41
42
  private resizePathsMap;
42
43
  private api;
43
44
  /**
@@ -76,6 +77,15 @@ export declare class LinkedServer extends Shape {
76
77
  * @todo Check if default file store is set, if not, set it
77
78
  */
78
79
  initStores(): Promise<any[]>;
80
+ /**
81
+ * Run a lifecycle hook (setupBeforeControllers, setupBeforeCatchAllControllers,
82
+ * setupAfterControllers) on every generic provider, in indexing order.
83
+ *
84
+ * Each provider is isolated: a hook that throws (or rejects) is logged with
85
+ * the package and hook name and the next provider still runs, so one broken
86
+ * provider does not abort boot. That provider's routes or middleware from
87
+ * that hook may be missing, but the rest of the app starts.
88
+ */
79
89
  callGenericBackendProvidersMethod(method: string, ...args: any[]): Promise<void>;
80
90
  /**
81
91
  * Removes temporary nodes from memory if they've been seen twice
@@ -84,6 +94,24 @@ export declare class LinkedServer extends Shape {
84
94
  */
85
95
  initGarbageCollection(): void;
86
96
  initBackendProviders(): Promise<void>;
97
+ /**
98
+ * Index the backend providers of every linked package in the app's
99
+ * dependency tree, dependencies first. Workspace members (`localPackages`)
100
+ * are also imported whole, as before; for anything else only its `/backend`
101
+ * entry is loaded, and a package without one costs a failed resolve.
102
+ */
103
+ private indexLinkedDependencyProviders;
104
+ /**
105
+ * Index a package's backend providers once. Shared by boot and the lazy
106
+ * path in callBackendMethod / callShapeMethod. `onSourceChange` bypasses it
107
+ * on purpose: an HMR reload must re-index.
108
+ */
109
+ ensurePackageBackendProviders(pkg: string, warnIfNotFound?: boolean): Promise<void>;
110
+ /**
111
+ * Node's `import()`, as a method so a test can resolve a fixture app's
112
+ * node_modules (a test runner resolves bare specifiers from this file).
113
+ */
114
+ protected importModule(specifier: string): Promise<any>;
87
115
  /**
88
116
  * Filters local workspace packages to only those reachable from the app's
89
117
  * dependency tree. Mirrors the logic used by `buildAll` in lincd-cli.
@@ -31,11 +31,48 @@ import { Shape } from '@_linked/core/shapes/Shape';
31
31
  import { LinkedErrorLogging } from '@_linked/core/utils/LinkedErrorLogging';
32
32
  import { FileStorePurposes, LinkedFileStorage, } from '@_linked/core/utils/LinkedFileStorage';
33
33
  import { getResizedImagesStore } from '../utils/resizedImagesPurpose.js';
34
+ import { createJsonBodyParser } from '../utils/jsonBodyParser.js';
34
35
  import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';
35
36
  import { autoLoadOntologyData } from '@_linked/core/utils/Package';
36
37
  import { getShapeClass, getSuperShapesClasses, } from '@_linked/core/utils/ShapeClass';
37
38
  import path from 'path';
38
39
  import { pathToFileURL } from 'node:url';
40
+ import { discoverLinkedDependencies, isBundledBySsr, ssrNoExternalOf, } from '../utils/linkedDependencies.js';
41
+ /**
42
+ * `instanceof ShapeProvider`, tolerating a second copy of server-utils.
43
+ *
44
+ * A registry-installed package's backend is loaded by Node (see
45
+ * isBundledBySsr), so in a dev runner that BUNDLES server-utils — the
46
+ * standalone case — its providers extend Node's copy of ShapeProvider, not the
47
+ * one this module imported. Fall back to the class name along the prototype
48
+ * chain. Production has one loader and one copy, so instanceof decides there.
49
+ */
50
+ function isShapeProvider(provider) {
51
+ var _a;
52
+ if (provider instanceof ShapeProvider)
53
+ return true;
54
+ for (let proto = provider ? Object.getPrototypeOf(provider) : null; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
55
+ if (((_a = proto.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'ShapeProvider')
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ /**
61
+ * Call one provider's hook and contain its failure: a synchronous throw or a
62
+ * rejection is logged, naming the package and the hook, and swallowed. Used
63
+ * for hooks that run for every provider (boot lifecycle hooks and per-request
64
+ * hooks), where one provider's bug must not take down boot or every request
65
+ * without saying which provider it was.
66
+ */
67
+ async function runProviderHook(pkg, hook, call) {
68
+ var _a;
69
+ try {
70
+ await call();
71
+ }
72
+ catch (err) {
73
+ console.error(chalk.red(`[linked] ${pkg} ${hook} failed: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`), (err === null || err === void 0 ? void 0 : err.stack) ? `\n${err.stack}` : '');
74
+ }
75
+ }
39
76
  import process from 'process';
40
77
  import * as React from 'react';
41
78
  import { renderToPipeableStream, renderToStaticMarkup } from 'react-dom/server';
@@ -125,6 +162,10 @@ let LinkedServer = class LinkedServer extends Shape {
125
162
  this.analyse = false;
126
163
  this.shapeProviders = new Map();
127
164
  this.genericProviders = new Map();
165
+ // One indexing per package: boot and the lazy /call path share it, so two
166
+ // concurrent first calls (or a call racing boot) never construct a
167
+ // package's providers twice.
168
+ this.providerIndexing = new Map();
128
169
  //from resizedFileName to full resized path (CDN or similar)
129
170
  this.resizePathsMap = new Map();
130
171
  if (config && typeof config !== 'string' && !('id' in config)) {
@@ -335,9 +376,8 @@ let LinkedServer = class LinkedServer extends Shape {
335
376
  // origin: ['http://localhost:4001', 'https://www.mynd.site'],
336
377
  // optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
337
378
  // };
338
- // //accept JSON bodies
339
- // this.server.use(bodyParser.json({limit: '50mb'}));
340
- this.server.use(express.json({ limit: '50mb' }));
379
+ //accept JSON bodies
380
+ this.server.use(createJsonBodyParser());
341
381
  // this.server.use(cors(corsOptions));
342
382
  //
343
383
  // //compress server output with gzip,
@@ -499,15 +539,21 @@ let LinkedServer = class LinkedServer extends Shape {
499
539
  return store.init ? store.init() : Promise.resolve();
500
540
  }));
501
541
  }
502
- // get generic backend providers for handle controller methods
542
+ /**
543
+ * Run a lifecycle hook (setupBeforeControllers, setupBeforeCatchAllControllers,
544
+ * setupAfterControllers) on every generic provider, in indexing order.
545
+ *
546
+ * Each provider is isolated: a hook that throws (or rejects) is logged with
547
+ * the package and hook name and the next provider still runs, so one broken
548
+ * provider does not abort boot. That provider's routes or middleware from
549
+ * that hook may be missing, but the rest of the app starts.
550
+ */
503
551
  async callGenericBackendProvidersMethod(method, ...args) {
504
- for (let genericProvider of this.genericProviders.values()) {
505
- if (!genericProvider || !genericProvider[method]) {
552
+ for (let [pkg, genericProvider] of this.genericProviders.entries()) {
553
+ if (!genericProvider || typeof genericProvider[method] !== 'function') {
506
554
  continue;
507
555
  }
508
- if (typeof genericProvider[method] == 'function') {
509
- await Promise.resolve(genericProvider[method](...args));
510
- }
556
+ await runProviderHook(pkg, method, () => genericProvider[method](...args));
511
557
  }
512
558
  }
513
559
  /**
@@ -535,16 +581,21 @@ let LinkedServer = class LinkedServer extends Shape {
535
581
  // // }, 1000 * 60 * 60); //every hour
536
582
  }
537
583
  async initBackendProviders() {
538
- //get all local workspace lincd packages, then filter to only those
539
- //in this app's dependency tree. This avoids loading npm-installed
540
- //legacy packages that may use an old version of the core.
584
+ // Local workspace packages in this app's dependency tree. These are also
585
+ // imported whole at boot (indexLincdPackage), not only their /backend.
541
586
  const allLocalPackages = getLincdPackages();
542
587
  const localPackageMap = new Map(allLocalPackages.map((pkg) => [pkg.packageName, pkg]));
543
588
  const relevantPackages = this.filterPackagesByDependencyTree(localPackageMap, this.package);
544
- for (let [pkgName, pkg] of relevantPackages) {
545
- await this.indexPackageBackendProviders(pkgName);
546
- await this.indexLincdPackage(pkgName);
547
- }
589
+ // Every linked package in the dependency tree, installed or local, each
590
+ // after the linked packages it depends on. Registry installs and
591
+ // localized checkouts are not workspace members, and used to be indexed
592
+ // only lazily, on the first /call/<pkg>/... A provider can hook every
593
+ // request (initRequest, supplyDataForRequest, setupBeforeControllers
594
+ // routes), so those hooks stayed silent until some RPC to the package
595
+ // happened — an installed @_linked/auth did not put the session into a
596
+ // page's request data until then, and its setupBeforeControllers
597
+ // middleware never registered at all.
598
+ await this.indexLinkedDependencyProviders(relevantPackages);
548
599
  try {
549
600
  await fs
550
601
  .readFile(path.join(process.cwd(), 'package.json'), 'utf-8')
@@ -569,6 +620,67 @@ let LinkedServer = class LinkedServer extends Shape {
569
620
  console.warn(err);
570
621
  }
571
622
  }
623
+ /**
624
+ * Index the backend providers of every linked package in the app's
625
+ * dependency tree, dependencies first. Workspace members (`localPackages`)
626
+ * are also imported whole, as before; for anything else only its `/backend`
627
+ * entry is loaded, and a package without one costs a failed resolve.
628
+ */
629
+ async indexLinkedDependencyProviders(localPackages) {
630
+ var _a;
631
+ const started = Date.now();
632
+ const installed = [];
633
+ const ordered = discoverLinkedDependencies(process.cwd(), this.package);
634
+ // A workspace member the walk cannot reach through node_modules still
635
+ // loads, after the rest, as it always did.
636
+ const remaining = new Set(localPackages.keys());
637
+ for (const { packageName } of ordered) {
638
+ const isLocal = remaining.delete(packageName);
639
+ await this.ensurePackageBackendProviders(packageName);
640
+ if (isLocal) {
641
+ await this.indexLincdPackage(packageName);
642
+ }
643
+ else if (this.genericProviders.get(packageName) ||
644
+ ((_a = this.shapeProviders.get(packageName)) === null || _a === void 0 ? void 0 : _a.length)) {
645
+ installed.push(packageName);
646
+ }
647
+ }
648
+ for (const packageName of remaining) {
649
+ await this.ensurePackageBackendProviders(packageName);
650
+ await this.indexLincdPackage(packageName);
651
+ }
652
+ if (installed.length > 0) {
653
+ console.log(chalk.gray(`[linked] indexed backend providers of ${ordered.length} linked package(s) in ${Date.now() - started}ms; installed ones with providers: ${installed.join(', ')}`));
654
+ }
655
+ }
656
+ /**
657
+ * Index a package's backend providers once. Shared by boot and the lazy
658
+ * path in callBackendMethod / callShapeMethod. `onSourceChange` bypasses it
659
+ * on purpose: an HMR reload must re-index.
660
+ */
661
+ async ensurePackageBackendProviders(pkg, warnIfNotFound = false) {
662
+ if (this.genericProviders.has(pkg) && this.shapeProviders.has(pkg)) {
663
+ return;
664
+ }
665
+ let indexing = this.providerIndexing.get(pkg);
666
+ if (!indexing) {
667
+ indexing = this.indexPackageBackendProviders(pkg, warnIfNotFound).then(() => undefined);
668
+ this.providerIndexing.set(pkg, indexing);
669
+ }
670
+ try {
671
+ await indexing;
672
+ }
673
+ finally {
674
+ this.providerIndexing.delete(pkg);
675
+ }
676
+ }
677
+ /**
678
+ * Node's `import()`, as a method so a test can resolve a fixture app's
679
+ * node_modules (a test runner resolves bare specifiers from this file).
680
+ */
681
+ importModule(specifier) {
682
+ return import(/* @vite-ignore */ specifier);
683
+ }
572
684
  /**
573
685
  * Filters local workspace packages to only those reachable from the app's
574
686
  * dependency tree. Mirrors the logic used by `buildAll` in lincd-cli.
@@ -897,7 +1009,7 @@ let LinkedServer = class LinkedServer extends Shape {
897
1009
  }
898
1010
  }
899
1011
  async indexPackageBackendProviders(pkg, warnIfNotFound = false, backendIndexFilePath) {
900
- var _a;
1012
+ var _a, _b;
901
1013
  if (!backendIndexFilePath) {
902
1014
  backendIndexFilePath = `${pkg}/backend`;
903
1015
  }
@@ -910,9 +1022,19 @@ let LinkedServer = class LinkedServer extends Shape {
910
1022
  // @semantu/create-now/backend), which Node's ESM resolver can't resolve
911
1023
  // from inside an installed package like @_linked/server.
912
1024
  const vite = (_a = this.config.server) === null || _a === void 0 ? void 0 : _a.vite;
1025
+ // In dev, load a package's backend with the loader that owns the package
1026
+ // (see isBundledBySsr): Vite for the app itself and for what the SSR
1027
+ // runner bundles (workspaces, SSR_ENTRY_PACKAGES), Node for what it
1028
+ // leaves external (registry installs). ssrLoadModule of an EXTERNAL
1029
+ // package's /backend evaluated that package a second time, apart from the
1030
+ // copy the app's own imports reach.
1031
+ const viteOwnsPackage = !!vite &&
1032
+ typeof vite.ssrLoadModule === 'function' &&
1033
+ (pkg === ((_b = this.package) === null || _b === void 0 ? void 0 : _b.name) ||
1034
+ isBundledBySsr(pkg, ssrNoExternalOf(vite)));
913
1035
  const loadModule = async (specifier) => {
914
1036
  var _a, _b, _c;
915
- if (vite && typeof vite.ssrLoadModule === 'function') {
1037
+ if (viteOwnsPackage) {
916
1038
  // For the user app's own backend, resolve to ./src/backend.ts directly.
917
1039
  // Vite reads the app's package.json exports.development condition.
918
1040
  if (specifier === `${this.package.name}/backend`) {
@@ -952,7 +1074,8 @@ let LinkedServer = class LinkedServer extends Shape {
952
1074
  }
953
1075
  return await vite.ssrLoadModule(specifier);
954
1076
  }
955
- // No-vite path (e.g. production runtime).
1077
+ // Node path: production runtime, or a package the dev SSR runner
1078
+ // leaves external.
956
1079
  //
957
1080
  // The app's own backend cannot be imported by name here. A self-reference
958
1081
  // resolves only from inside the package that declares it, and this code
@@ -967,7 +1090,7 @@ let LinkedServer = class LinkedServer extends Shape {
967
1090
  return await import(/* @vite-ignore */ pathToFileURL(target).href);
968
1091
  }
969
1092
  // Fall back to Node's resolver. The dynamic specifier is intentional.
970
- return await import(/* @vite-ignore */ specifier);
1093
+ return await this.importModule(specifier);
971
1094
  };
972
1095
  await loadModule(backendIndexFilePath)
973
1096
  .then((backendProviderExports) => {
@@ -977,7 +1100,7 @@ let LinkedServer = class LinkedServer extends Shape {
977
1100
  //always send an instance of the express server
978
1101
  //TODO: do not create an instance, just save the class and instantiate it when needed
979
1102
  let provider = new providerClass(this.server, this);
980
- if (provider instanceof ShapeProvider) {
1103
+ if (isShapeProvider(provider)) {
981
1104
  shapeProviders.push(provider);
982
1105
  if (!Object.getOwnPropertyNames(provider).includes('shape')) {
983
1106
  console.warn(chalk.red(`${Object.getPrototypeOf(provider).constructor.name} in package ${pkg}
@@ -1192,7 +1315,7 @@ let LinkedServer = class LinkedServer extends Shape {
1192
1315
  var _a;
1193
1316
  //- index module providers if not done yet
1194
1317
  if (!this.shapeProviders.has(pkg)) {
1195
- await this.indexPackageBackendProviders(pkg, true);
1318
+ await this.ensurePackageBackendProviders(pkg, true);
1196
1319
  }
1197
1320
  let packageShapeProviders = this.shapeProviders.get(pkg);
1198
1321
  let findProviderForShape = (nodeShapeId) => {
@@ -1200,7 +1323,7 @@ let LinkedServer = class LinkedServer extends Shape {
1200
1323
  var _a, _b;
1201
1324
  //access the static shape (which is a linkedShape() / Shape class)
1202
1325
  //then access the SHACL NodeShape of that shape class, and its id
1203
- return (provider instanceof ShapeProvider &&
1326
+ return (isShapeProvider(provider) &&
1204
1327
  ((_b = (_a = provider.shape) === null || _a === void 0 ? void 0 : _a.shape) === null || _b === void 0 ? void 0 : _b.id) === nodeShapeId);
1205
1328
  });
1206
1329
  };
@@ -1649,32 +1772,29 @@ let LinkedServer = class LinkedServer extends Shape {
1649
1772
  res.json(jsonObject);
1650
1773
  }
1651
1774
  async initRequest(request, response) {
1652
- // initialise the request for all providers, do it synchroniously, one after the other
1653
- let p = Promise.resolve();
1654
- [...this.genericProviders.values()]
1655
- .filter(Boolean)
1656
- .forEach((backendProvider) => {
1657
- p = p
1658
- .then(() => {
1659
- return backendProvider.initRequest(request, response);
1660
- })
1661
- .catch((err) => {
1662
- console.warn(`Error during initRequest for provider ${Object.getPrototypeOf(backendProvider).constructor.name}: `, err);
1663
- });
1664
- });
1665
- return p;
1775
+ // initialise the request for all providers, one after the other (order
1776
+ // matters: later providers may read what earlier ones put on the request).
1777
+ // A provider that throws is logged by package name and skipped.
1778
+ for (const [pkg, backendProvider] of this.genericProviders.entries()) {
1779
+ if (!backendProvider || typeof backendProvider.initRequest !== 'function') {
1780
+ continue;
1781
+ }
1782
+ await runProviderHook(pkg, 'initRequest', () => backendProvider.initRequest(request, response));
1783
+ }
1666
1784
  }
1667
1785
  async getRequestData(request, response) {
1668
- // Phase 1: providers return plain JSON data via supplyDataForRequest.
1669
- // requestLD is kept as empty string for now — SSR data seeding will be
1670
- // reworked in Phase 2/3 to inject query results instead of graph data.
1786
+ // Providers return plain JSON data via supplyDataForRequest.
1787
+ // requestLD is kept as empty string for now; SSR data seeding will
1788
+ // inject query results instead of graph data.
1789
+ // A provider that throws (synchronously or not) is logged by package
1790
+ // name; the page still renders with the other providers' data.
1671
1791
  let requestData = {};
1672
- await Promise.all([...this.genericProviders.values()].map((backendProvider) => {
1673
- if (backendProvider) {
1674
- return Promise.resolve(backendProvider.supplyDataForRequest(request, response, requestData)).catch((err) => {
1675
- console.warn(`Error requesting page-request data from ${Object.getPrototypeOf(backendProvider).constructor.name}: `, err);
1676
- });
1792
+ await Promise.all([...this.genericProviders.entries()].map(([pkg, backendProvider]) => {
1793
+ if (!backendProvider ||
1794
+ typeof backendProvider.supplyDataForRequest !== 'function') {
1795
+ return;
1677
1796
  }
1797
+ return runProviderHook(pkg, 'supplyDataForRequest', () => backendProvider.supplyDataForRequest(request, response, requestData));
1678
1798
  }));
1679
1799
  let requestLD = '';
1680
1800
  let requestObject = JSONWriter.stringify(request['frontendData']);
@@ -1682,7 +1802,7 @@ let LinkedServer = class LinkedServer extends Shape {
1682
1802
  }
1683
1803
  async callBackendMethod(pkg, method, args, request, response) {
1684
1804
  if (!this.genericProviders.has(pkg)) {
1685
- await this.indexPackageBackendProviders(pkg, true);
1805
+ await this.ensurePackageBackendProviders(pkg, true);
1686
1806
  }
1687
1807
  //retrieve the indexed provider class and create a new instance for this request
1688
1808
  let genericBackendProvider = this.genericProviders.get(pkg);