@_linked/core 2.33.0 → 2.34.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.34.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#344](https://github.com/linked-fw/core/pull/344) [`705b1fa`](https://github.com/linked-fw/core/commit/705b1fa1d82a8933ee8b4a9d4791b8eb7764874b) Thanks [@flyon](https://github.com/flyon)! - `loadStores` imports stores through a host-supplied loader when one is set. A host that runs the app through another module loader puts a function `(specifier: string) => Promise<unknown>` on `globalThis[Symbol.for('@_linked/core:importModule')]`; `loadStores` calls it with each entry's `store` specifier unchanged and builds the store from the namespace it resolves to. Without it, `loadStores` uses the native `import()` as before. `linked start` (dev) sets it so a store named in a datasets file comes from the same copy of its package as the app's own import of it.
8
+
3
9
  ## 2.33.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -522,6 +522,8 @@ loadStores<T>(config: DatasetsConfig): Promise<Record<string, T>>
522
522
 
523
523
  Dynamically imports each alias's `store` path and instantiates the resolved class with `new StoreClass(entry.config)`. Returns alias → store. Convention: the last segment of the `store` path is the named export to use (with `default` as a fallback). **Async + uses runtime dynamic `import()`** — works in Node where module specifiers resolve at runtime. Frontends can't use this (webpack can't bundle `import(variableString)`); frontend code instead imports each store class statically and constructs per alias by hand.
524
524
 
525
+ **Host loader.** When `globalThis[Symbol.for('@_linked/core:importModule')]` is a function `(specifier: string) => Promise<unknown>`, `loadStores` imports each store through it instead of `import()`, passing the `store` specifier unchanged; it is read on every call. `linked start` (dev) sets it so a store comes from the same copy of its package as the app's own imports under Vite's SSR runner. Nothing else needs to set it.
526
+
525
527
  A typical backend setup looks like:
526
528
 
527
529
  ```ts
@@ -9,6 +9,10 @@ import type { DatasetsConfig } from './parseDatasetsConfig.js';
9
9
  * Falls back to the module's `default` export if the named export is
10
10
  * absent. Each store class must accept a single config-object argument.
11
11
  *
12
+ * Imports through the host loader under
13
+ * `Symbol.for('@_linked/core:importModule')` when one is set (see
14
+ * `importModule` above), else the native `import()`.
15
+ *
12
16
  * Async + uses runtime dynamic import; works in Node where the module
13
17
  * specifier can be resolved at runtime. Frontends should instead
14
18
  * import each store class explicitly in their storage-config file and
@@ -13,6 +13,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
13
13
  step((generator = generator.apply(thisArg, _arguments || [])).next());
14
14
  });
15
15
  };
16
+ /**
17
+ * The module loader `loadStores` imports each store with. A host that
18
+ * evaluates the app through another loader sets it on `globalThis`, so a
19
+ * store comes from the same copy of its package as the app's own import of
20
+ * it. `linked start` (dev) sets it, because there the app runs in Vite's SSR
21
+ * module runner while a native `import()` would load a second copy through
22
+ * Node (cli backlog 014). Unset, `loadStores` uses the native `import()`.
23
+ *
24
+ * The hook receives the specifier unchanged and resolves to the module
25
+ * namespace. It is read on every call: the host may set it after this module
26
+ * was evaluated.
27
+ */
28
+ const IMPORT_MODULE = Symbol.for('@_linked/core:importModule');
29
+ function importModule(specifier) {
30
+ const hook = globalThis[IMPORT_MODULE];
31
+ return typeof hook === 'function'
32
+ ? hook(specifier)
33
+ : import(specifier);
34
+ }
16
35
  /**
17
36
  * Dynamically imports each alias's `store` (an npm specifier) and
18
37
  * instantiates the resolved class with the entry's `config` verbatim —
@@ -23,6 +42,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
23
42
  * Falls back to the module's `default` export if the named export is
24
43
  * absent. Each store class must accept a single config-object argument.
25
44
  *
45
+ * Imports through the host loader under
46
+ * `Symbol.for('@_linked/core:importModule')` when one is set (see
47
+ * `importModule` above), else the native `import()`.
48
+ *
26
49
  * Async + uses runtime dynamic import; works in Node where the module
27
50
  * specifier can be resolved at runtime. Frontends should instead
28
51
  * import each store class explicitly in their storage-config file and
@@ -54,7 +77,7 @@ export function loadStores(config) {
54
77
  // It must come from a trusted developer artifact (datasets.json) and must
55
78
  // NEVER be attacker-influenced — an arbitrary specifier is arbitrary code
56
79
  // execution. Do not source this from request/user input.
57
- const mod = yield import(entry.store);
80
+ const mod = yield importModule(entry.store);
58
81
  const exportName = entry.store.split('/').pop();
59
82
  const StoreClass = (_a = (mod && mod[exportName])) !== null && _a !== void 0 ? _a : (mod && mod.default);
60
83
  if (typeof StoreClass !== 'function') {
@@ -1 +1 @@
1
- {"version":3,"file":"loadStores.js","sourceRoot":"","sources":["../../../src/utils/loadStores.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,qEAAqE;AACrE,wEAAwE;AACxE,qEAAqE;AACrE,EAAE;AACF,qDAAqD;;;;;;;;;;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,UAAgB,UAAU,CAC9B,MAAsB;;;QAEtB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7D,0EAA0E;YAC1E,0EAA0E;YAC1E,0EAA0E;YAC1E,yDAAyD;YACzD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YACtC,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC;YACjD,MAAM,UAAU,GACd,MAAA,CAAC,GAAG,IAAK,GAA+B,CAAC,UAAU,CAAC,CAAC,mCACrD,CAAC,GAAG,IAAK,GAA2B,CAAC,OAAO,CAAC,CAAC;YAChD,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;gBACrC,MAAM,IAAI,KAAK,CACb,sDAAsD,KAAK,CAAC,KAAK,gBAAgB,KAAK,KAAK;oBACzF,4BAA4B,UAAU,wCAAwC,CACjF,CAAC;YACJ,CAAC;YACD,MAAM,CAAC,KAAK,CAAC,GAAG,IAAK,UAAsC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CAAA","sourcesContent":["// Backend-side store loader. Lives in its own file so that frontend\n// bundlers (webpack) consuming parseDatasetsConfig don't pull in the\n// dynamic-import code below — webpack flags `import(variableString)` as\n// a critical dependency when it can't analyse the target statically.\n//\n// Backends import this directly; frontends never do.\n\nimport type {DatasetsConfig} from './parseDatasetsConfig.js';\n\n/**\n * Dynamically imports each alias's `store` (an npm specifier) and\n * instantiates the resolved class with the entry's `config` verbatim —\n * `new StoreClass(entry.config)`. Returns alias → store.\n *\n * Convention: the last segment of the `store` path is the named export\n * to use (e.g. `@_linked/fuseki/shapes/FusekiStore` → `FusekiStore`).\n * Falls back to the module's `default` export if the named export is\n * absent. Each store class must accept a single config-object argument.\n *\n * Async + uses runtime dynamic import; works in Node where the module\n * specifier can be resolved at runtime. Frontends should instead\n * import each store class explicitly in their storage-config file and\n * instantiate per alias by hand (webpack can't bundle\n * `import(variableString)`).\n *\n * @example\n * ```ts\n * import rawConfig from './linked.backend.datasets.json' assert { type: 'json' };\n * import { parseDatasetsConfig } from '@_linked/core/utils/parseDatasetsConfig';\n * import { loadStores } from '@_linked/core/utils/loadStores';\n * import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\n *\n * const config = parseDatasetsConfig(rawConfig, process.env);\n * const stores = await loadStores(config);\n * LinkedStorage.setDefaultDataset(stores.appData);\n * ```\n *\n * @param config Parsed config from {@link parseDatasetsConfig}.\n * @returns Map of alias → constructed store instance.\n * @throws If a `store` path can't be resolved to a class export.\n */\nexport async function loadStores<T = unknown>(\n config: DatasetsConfig,\n): Promise<Record<string, T>> {\n const stores: Record<string, T> = {};\n for (const [alias, entry] of Object.entries(config.datasets)) {\n // SECURITY: `entry.store` is a module specifier loaded and executed here.\n // It must come from a trusted developer artifact (datasets.json) and must\n // NEVER be attacker-influenced — an arbitrary specifier is arbitrary code\n // execution. Do not source this from request/user input.\n const mod = await import(entry.store);\n const exportName = entry.store.split('/').pop()!;\n const StoreClass =\n (mod && (mod as Record<string, unknown>)[exportName]) ??\n (mod && (mod as {default?: unknown}).default);\n if (typeof StoreClass !== 'function') {\n throw new Error(\n `loadStores: could not resolve a class export from \"${entry.store}\" for alias \"${alias}\". ` +\n `Expected a named export \"${exportName}\" or a default export that is a class.`,\n );\n }\n stores[alias] = new (StoreClass as new (cfg: unknown) => T)(entry.config);\n }\n return stores;\n}\n"]}
1
+ {"version":3,"file":"loadStores.js","sourceRoot":"","sources":["../../../src/utils/loadStores.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,qEAAqE;AACrE,wEAAwE;AACxE,qEAAqE;AACrE,EAAE;AACF,qDAAqD;;;;;;;;;;AAIrD;;;;;;;;;;;GAWG;AACH,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;AAE/D,SAAS,YAAY,CAAC,SAAiB;IACrC,MAAM,IAAI,GAAI,UAAsC,CAAC,aAAa,CAAC,CAAC;IACpE,OAAO,OAAO,IAAI,KAAK,UAAU;QAC/B,CAAC,CAAE,IAAgD,CAAC,SAAS,CAAC;QAC9D,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AACxB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAgB,UAAU,CAC9B,MAAsB;;;QAEtB,MAAM,MAAM,GAAsB,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC7D,0EAA0E;YAC1E,0EAA0E;YAC1E,0EAA0E;YAC1E,yDAAyD;YACzD,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAC5C,MAAM,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC;YACjD,MAAM,UAAU,GACd,MAAA,CAAC,GAAG,IAAK,GAA+B,CAAC,UAAU,CAAC,CAAC,mCACrD,CAAC,GAAG,IAAK,GAA2B,CAAC,OAAO,CAAC,CAAC;YAChD,IAAI,OAAO,UAAU,KAAK,UAAU,EAAE,CAAC;gBACrC,MAAM,IAAI,KAAK,CACb,sDAAsD,KAAK,CAAC,KAAK,gBAAgB,KAAK,KAAK;oBACzF,4BAA4B,UAAU,wCAAwC,CACjF,CAAC;YACJ,CAAC;YACD,MAAM,CAAC,KAAK,CAAC,GAAG,IAAK,UAAsC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5E,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CAAA","sourcesContent":["// Backend-side store loader. Lives in its own file so that frontend\n// bundlers (webpack) consuming parseDatasetsConfig don't pull in the\n// dynamic-import code below — webpack flags `import(variableString)` as\n// a critical dependency when it can't analyse the target statically.\n//\n// Backends import this directly; frontends never do.\n\nimport type {DatasetsConfig} from './parseDatasetsConfig.js';\n\n/**\n * The module loader `loadStores` imports each store with. A host that\n * evaluates the app through another loader sets it on `globalThis`, so a\n * store comes from the same copy of its package as the app's own import of\n * it. `linked start` (dev) sets it, because there the app runs in Vite's SSR\n * module runner while a native `import()` would load a second copy through\n * Node (cli backlog 014). Unset, `loadStores` uses the native `import()`.\n *\n * The hook receives the specifier unchanged and resolves to the module\n * namespace. It is read on every call: the host may set it after this module\n * was evaluated.\n */\nconst IMPORT_MODULE = Symbol.for('@_linked/core:importModule');\n\nfunction importModule(specifier: string): Promise<unknown> {\n const hook = (globalThis as Record<symbol, unknown>)[IMPORT_MODULE];\n return typeof hook === 'function'\n ? (hook as (specifier: string) => Promise<unknown>)(specifier)\n : import(specifier);\n}\n\n/**\n * Dynamically imports each alias's `store` (an npm specifier) and\n * instantiates the resolved class with the entry's `config` verbatim —\n * `new StoreClass(entry.config)`. Returns alias → store.\n *\n * Convention: the last segment of the `store` path is the named export\n * to use (e.g. `@_linked/fuseki/shapes/FusekiStore` → `FusekiStore`).\n * Falls back to the module's `default` export if the named export is\n * absent. Each store class must accept a single config-object argument.\n *\n * Imports through the host loader under\n * `Symbol.for('@_linked/core:importModule')` when one is set (see\n * `importModule` above), else the native `import()`.\n *\n * Async + uses runtime dynamic import; works in Node where the module\n * specifier can be resolved at runtime. Frontends should instead\n * import each store class explicitly in their storage-config file and\n * instantiate per alias by hand (webpack can't bundle\n * `import(variableString)`).\n *\n * @example\n * ```ts\n * import rawConfig from './linked.backend.datasets.json' assert { type: 'json' };\n * import { parseDatasetsConfig } from '@_linked/core/utils/parseDatasetsConfig';\n * import { loadStores } from '@_linked/core/utils/loadStores';\n * import { LinkedStorage } from '@_linked/core/utils/LinkedStorage';\n *\n * const config = parseDatasetsConfig(rawConfig, process.env);\n * const stores = await loadStores(config);\n * LinkedStorage.setDefaultDataset(stores.appData);\n * ```\n *\n * @param config Parsed config from {@link parseDatasetsConfig}.\n * @returns Map of alias → constructed store instance.\n * @throws If a `store` path can't be resolved to a class export.\n */\nexport async function loadStores<T = unknown>(\n config: DatasetsConfig,\n): Promise<Record<string, T>> {\n const stores: Record<string, T> = {};\n for (const [alias, entry] of Object.entries(config.datasets)) {\n // SECURITY: `entry.store` is a module specifier loaded and executed here.\n // It must come from a trusted developer artifact (datasets.json) and must\n // NEVER be attacker-influenced — an arbitrary specifier is arbitrary code\n // execution. Do not source this from request/user input.\n const mod = await importModule(entry.store);\n const exportName = entry.store.split('/').pop()!;\n const StoreClass =\n (mod && (mod as Record<string, unknown>)[exportName]) ??\n (mod && (mod as {default?: unknown}).default);\n if (typeof StoreClass !== 'function') {\n throw new Error(\n `loadStores: could not resolve a class export from \"${entry.store}\" for alias \"${alias}\". ` +\n `Expected a named export \"${exportName}\" or a default export that is a class.`,\n );\n }\n stores[alias] = new (StoreClass as new (cfg: unknown) => T)(entry.config);\n }\n return stores;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@_linked/core",
3
- "version": "2.33.0",
3
+ "version": "2.34.0",
4
4
  "packageManager": "npm@12.2.0",
5
5
  "license": "MIT",
6
6
  "linkedPackage": true,