@quo-systems/dock 0.2.5 → 0.2.6

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.
@@ -25,6 +25,7 @@ import { mkdir, readFile, writeFile, unlink, stat } from 'node:fs/promises';
25
25
  import { existsSync } from 'node:fs';
26
26
  import { join, resolve, isAbsolute } from 'node:path';
27
27
  import { pathToFileURL } from 'node:url';
28
+ import { register } from 'node:module';
28
29
  import process from 'node:process';
29
30
  import { User, Desk, Avatar } from '../beings/index.js';
30
31
  import { setup } from '../beings/setup.js';
@@ -35,12 +36,24 @@ export const BUILT_IN = { User, Desk, Avatar };
35
36
  // a second harbor in the same pid would pass that check while still being a
36
37
  // second ward with one pk.
37
38
  const HELD = new Set();
39
+ // A class file in the harbor folder resolves its packages from where the
40
+ // dock is installed, `resolve.ts`: the folder has no node_modules and is not
41
+ // meant to. Registered once per process, before the first class loads.
42
+ let hooked = false;
43
+ function hook() {
44
+ if (hooked)
45
+ return;
46
+ hooked = true;
47
+ const self = import.meta.url;
48
+ register(new URL(self.endsWith('.ts') ? './resolve.ts' : './resolve.js', self), { parentURL: self, data: { parent: self } });
49
+ }
38
50
  // The class source is a module. Every export that is a class is a class the
39
51
  // harbor holds, under its export name. The harbor never sees a body: it
40
52
  // constructs when a ward names a class, and never chooses one.
41
53
  async function loadClasses(path) {
42
54
  if (!existsSync(path))
43
55
  return {};
56
+ hook();
44
57
  // Keyed by the file's own time, so a source that changed on disk is read
45
58
  // again and a harbor boots on what it pins, not on what it once loaded.
46
59
  const { mtimeMs } = await stat(path);
@@ -0,0 +1,16 @@
1
+ type Context = {
2
+ parentURL?: string;
3
+ conditions: string[];
4
+ importAttributes: Record<string, string>;
5
+ };
6
+ type Resolved = {
7
+ url: string;
8
+ format?: string | null;
9
+ shortCircuit?: boolean;
10
+ };
11
+ type Next = (specifier: string, context: Context) => Promise<Resolved>;
12
+ export declare function initialize(data: {
13
+ parent: string;
14
+ }): void;
15
+ export declare function resolve(specifier: string, context: Context, next: Next): Promise<Resolved>;
16
+ export {};
@@ -0,0 +1,17 @@
1
+ let parent = '';
2
+ export function initialize(data) {
3
+ parent = data.parent;
4
+ }
5
+ const bare = (s) => !s.startsWith('.') && !s.startsWith('/') && !/^[a-z]+:/i.test(s);
6
+ export async function resolve(specifier, context, next) {
7
+ try {
8
+ return await next(specifier, context);
9
+ }
10
+ catch (e) {
11
+ const missing = e.code === 'ERR_MODULE_NOT_FOUND';
12
+ const from = context.parentURL ?? '';
13
+ if (missing && parent && bare(specifier) && from.startsWith('file:') && !from.includes('/node_modules/'))
14
+ return next(specifier, { ...context, parentURL: parent });
15
+ throw e;
16
+ }
17
+ }
package/harbor/disk.ts CHANGED
@@ -17,6 +17,7 @@ import { mkdir, readFile, writeFile, unlink, stat } from 'node:fs/promises';
17
17
  import { existsSync } from 'node:fs';
18
18
  import { join, resolve, isAbsolute } from 'node:path';
19
19
  import { pathToFileURL } from 'node:url';
20
+ import { register } from 'node:module';
20
21
  import process from 'node:process';
21
22
  import type { BeingClass } from '@quo-systems/quo';
22
23
  import { User, Desk, Avatar } from '../beings/index.ts';
@@ -33,11 +34,23 @@ export const BUILT_IN: Record<string, BeingClass> = { User, Desk, Avatar };
33
34
  // second ward with one pk.
34
35
  const HELD = new Set<string>();
35
36
 
37
+ // A class file in the harbor folder resolves its packages from where the
38
+ // dock is installed, `resolve.ts`: the folder has no node_modules and is not
39
+ // meant to. Registered once per process, before the first class loads.
40
+ let hooked = false;
41
+ function hook(): void {
42
+ if (hooked) return;
43
+ hooked = true;
44
+ const self = import.meta.url;
45
+ register(new URL(self.endsWith('.ts') ? './resolve.ts' : './resolve.js', self), { parentURL: self, data: { parent: self } });
46
+ }
47
+
36
48
  // The class source is a module. Every export that is a class is a class the
37
49
  // harbor holds, under its export name. The harbor never sees a body: it
38
50
  // constructs when a ward names a class, and never chooses one.
39
51
  async function loadClasses(path: string): Promise<Record<string, BeingClass>> {
40
52
  if (!existsSync(path)) return {};
53
+ hook();
41
54
  // Keyed by the file's own time, so a source that changed on disk is read
42
55
  // again and a harbor boots on what it pins, not on what it once loaded.
43
56
  const { mtimeMs } = await stat(path);
@@ -339,6 +339,15 @@ core plus files, the loader and the pid, and holds the `ws` listener's end of
339
339
  every socket dialed to it; the browser harbor, `browser.ts`, is the core plus
340
340
  IndexedDB, the built-in beings, the lock and one dialer per world.
341
341
 
342
+ The harbor folder is the device's and not a package: it has no node_modules
343
+ and is not meant to, and a class file in it imports the library by name like
344
+ any code. So the disk harbor resolves a class file's packages from where the
345
+ dock itself is installed, `resolve.ts`, a module resolution hook registered
346
+ once before the first class loads: a bare name that does not resolve from a
347
+ file outside any node_modules is resolved again from the dock's own place.
348
+ A class sees the packages the daemon sees, the library and the dock's own,
349
+ and one copy of each, so a `Being` in a class file is the daemon's `Being`.
350
+
342
351
  A tab's store keeps values through JSON, as a file does, because the ward
343
352
  hands its partition out through a guard that structured clone refuses.
344
353
  The daemon's `/quo` and `/health` answer any origin, with the preflight a
@@ -0,0 +1,31 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // How a class file the harbor folder holds finds its packages. The folder
3
+ // is the device's, not a package: it has no node_modules, and a class in it
4
+ // that imports `@quo-systems/quo` would otherwise fail to resolve from where
5
+ // it sits. This is a module resolution hook, registered once by the disk
6
+ // harbor: a bare specifier that does not resolve from a file outside any
7
+ // node_modules is resolved again from where the dock itself is installed,
8
+ // so a class file sees exactly the packages the daemon sees, the library
9
+ // and the dock's own, and one copy of each.
10
+ type Context = { parentURL?: string; conditions: string[]; importAttributes: Record<string, string> };
11
+ type Resolved = { url: string; format?: string | null; shortCircuit?: boolean };
12
+ type Next = (specifier: string, context: Context) => Promise<Resolved>;
13
+
14
+ let parent = '';
15
+
16
+ export function initialize(data: { parent: string }): void {
17
+ parent = data.parent;
18
+ }
19
+
20
+ const bare = (s: string) => !s.startsWith('.') && !s.startsWith('/') && !/^[a-z]+:/i.test(s);
21
+
22
+ export async function resolve(specifier: string, context: Context, next: Next): Promise<Resolved> {
23
+ try {
24
+ return await next(specifier, context);
25
+ } catch (e) {
26
+ const missing = (e as { code?: string }).code === 'ERR_MODULE_NOT_FOUND';
27
+ const from = context.parentURL ?? '';
28
+ if (missing && parent && bare(specifier) && from.startsWith('file:') && !from.includes('/node_modules/')) return next(specifier, { ...context, parentURL: parent });
29
+ throw e;
30
+ }
31
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quo-systems/dock",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "The dock: what every estate on Quo needs and nobody writes twice. A daemon and the quo command, the front desk, the user being and the avatar, harbors on disk, in a tab and on the edge, the model sides and the screen.",
5
5
  "keywords": [
6
6
  "quo",
@@ -72,7 +72,7 @@
72
72
  "build": "rm -rf dist && tsc -p tsconfig.build.json && cp harbor/edge/platform.d.ts dist/harbor/edge/ && cp -R cli/estate dist/cli/",
73
73
  "test": "node --test \"test/*.test.ts\"",
74
74
  "check:terrain": "node --test \"test/terrain/*.test.ts\"",
75
- "prepublishOnly": "cd ../.. && npm run check && npm run check:terrain"
75
+ "prepublishOnly": "test \"$QUO_GATED\" = 1 || { echo 'publish from the root, gated once: npm run release' >&2; exit 1; }"
76
76
  },
77
77
  "dependencies": {
78
78
  "@aparajita/capacitor-secure-storage": "^8.0.0",
@@ -80,7 +80,7 @@
80
80
  "@capacitor/core": "^8.5.1",
81
81
  "@capacitor/filesystem": "^8.1.3",
82
82
  "@modelcontextprotocol/sdk": "^1.30.0",
83
- "@quo-systems/quo": "^0.2.5",
83
+ "@quo-systems/quo": "^0.2.6",
84
84
  "esbuild": "^0.28.2",
85
85
  "ws": "^8.21.3"
86
86
  },