nervur 0.20.2 → 0.20.4

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/src/node/index.ts CHANGED
@@ -13,10 +13,11 @@
13
13
  //
14
14
  // One run holds a folder: a terrain refuses a folder another run serves.
15
15
  import { createHash } from 'node:crypto';
16
- import { chmod, rm } from 'node:fs/promises';
16
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
17
+ import { createRequire } from 'node:module';
17
18
  import { connect, createServer, type Server } from 'node:net';
18
19
  import { homedir, tmpdir } from 'node:os';
19
- import { join, resolve } from 'node:path';
20
+ import { dirname, join, resolve } from 'node:path';
20
21
  import { pathToFileURL } from 'node:url';
21
22
  import type { JsonObject } from '../being/index.ts';
22
23
  import { Loader, type Loaded, type Module } from '../contract/index.ts';
@@ -43,36 +44,99 @@ export const socketOf = (dir: string): string => {
43
44
  return join(tmpdir(), `nervur-${createHash('sha256').update(resolve(dir)).digest('hex').slice(0, 32)}.sock`);
44
45
  };
45
46
 
46
- // Code from a file: a module that exports `module`, `version` and
47
- // `classes`, found again by its absolute path. A file that cannot be
48
- // imported, or exports no module, is none.
47
+ // A specifier that names a file rather than a package.
48
+ export const isFile = (from: string): boolean => /^(\.{1,2}[\\/]|[\\/]|[A-Za-z]:[\\/])/.test(from) || /\.(m?[jt]s|c[jt]s)$/.test(from);
49
+
50
+ // Code from a file or an installed package: a module that exports
51
+ // `module`, `version` and `classes`, found again by where it was found, a
52
+ // file by its absolute path and a package by its name, resolved from each
53
+ // folder of `bases` in order. What cannot be imported, or exports no
54
+ // module, is none.
49
55
  export class ImportLoader extends Loader {
56
+ readonly #bases: readonly string[];
57
+ constructor(bases: readonly string[] = [process.cwd()]) {
58
+ super();
59
+ this.#bases = bases;
60
+ }
61
+
50
62
  async load(from: string): Promise<Loaded | null> {
51
- const path = resolve(from);
63
+ const found = isFile(from) ? resolve(from) : this.#package(from);
64
+ if (found === null) return null;
52
65
  let exported: { module?: unknown; version?: unknown; classes?: unknown };
53
66
  try {
54
- exported = (await import(pathToFileURL(path).href)) as typeof exported;
67
+ exported = (await import(pathToFileURL(found).href)) as typeof exported;
55
68
  } catch {
56
69
  return null;
57
70
  }
58
71
  const { module, version, classes } = exported;
59
72
  if (typeof module !== 'string' || typeof version !== 'string' || !Array.isArray(classes)) return null;
60
- return { module, version, classes, from: path };
73
+ return { module, version, classes, from: isFile(from) ? found : from };
74
+ }
75
+
76
+ #package(name: string): string | null {
77
+ for (const base of this.#bases) {
78
+ try {
79
+ return createRequire(join(resolve(base), 'noop.js')).resolve(name);
80
+ } catch {
81
+ // Not installed from here; the next base may hold it.
82
+ }
83
+ }
84
+ return null;
61
85
  }
62
86
  }
63
87
 
64
- // The modules a harbor is handed, by name, and then files.
88
+ // A module by its id, the name it declares, so the catalogue's cells name
89
+ // code the same way on every ground. `add` may name a module by its id, a
90
+ // file, or an installed package, and the ground works out which. The
91
+ // modules a harbor is handed are found by their id first. Where else an id
92
+ // is found, a file's path or a package's name, is this ground's own
93
+ // knowledge, kept beside the harbor in `<dir>/modules.json` and never in
94
+ // the sealed package, since another ground finds the same id in its own
95
+ // bundle. Packages resolve from where the command runs, then from the
96
+ // harbor's folder.
65
97
  export class NodeLoader extends ImportLoader {
66
98
  readonly #held: HeldLoader;
67
- constructor(modules: readonly Module[] = []) {
68
- super();
99
+ readonly #map: string | undefined;
100
+ constructor(modules: readonly Module[] = [], dir?: string) {
101
+ super(dir === undefined ? [process.cwd()] : [process.cwd(), dir]);
69
102
  this.#held = new HeldLoader(modules);
103
+ this.#map = dir === undefined ? undefined : join(dir, MODULES);
70
104
  }
105
+
71
106
  override async load(from: string): Promise<Loaded | null> {
72
- return (await this.#held.load(from)) ?? super.load(from);
107
+ const held = await this.#held.load(from);
108
+ if (held) return held;
109
+ const known = await this.#known();
110
+ const where = known[from];
111
+ if (where !== undefined) {
112
+ const loaded = await super.load(where);
113
+ return loaded?.module === from ? { ...loaded, from } : null;
114
+ }
115
+ const loaded = await super.load(from);
116
+ if (!loaded) return null;
117
+ if (this.#map && known[loaded.module] !== loaded.from) await this.#keep({ ...known, [loaded.module]: loaded.from });
118
+ return { ...loaded, from: loaded.module };
119
+ }
120
+
121
+ async #known(): Promise<Record<string, string>> {
122
+ if (!this.#map) return {};
123
+ try {
124
+ return JSON.parse(await readFile(this.#map, 'utf8')) as Record<string, string>;
125
+ } catch {
126
+ return {};
127
+ }
128
+ }
129
+
130
+ async #keep(files: Record<string, string>): Promise<void> {
131
+ await mkdir(dirname(this.#map!), { recursive: true, mode: 0o700 });
132
+ await writeFile(`${this.#map!}.new`, `${JSON.stringify(files, null, 2)}\n`, { mode: 0o600 });
133
+ await rename(`${this.#map!}.new`, this.#map!);
73
134
  }
74
135
  }
75
136
 
137
+ // Where a Node harbor's module ids find their files.
138
+ export const MODULES = 'modules.json';
139
+
76
140
  // One root request over a served harbor's socket, or undefined where none
77
141
  // is served.
78
142
  export const askServed = (dir: string, request: RootRequest): Promise<JsonObject | undefined> =>
@@ -107,7 +171,7 @@ export class NodeTerrain extends PointerTerrain {
107
171
  const entropy = new CryptoEntropy();
108
172
  super({
109
173
  entropy,
110
- loader: new NodeLoader(parts.modules),
174
+ loader: new NodeLoader(parts.modules, parts.dir),
111
175
  custody: new FolderCustody(parts.dir, entropy),
112
176
  memory: new FolderMemory(parts.dir),
113
177
  faculties: [tcpFaculty, webGround, webPush],