@fougere/registry 0.0.0-stage → 0.23.0-alpha.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/LICENSE +21 -0
- package/README.md +42 -2
- package/dist/Registry.d.ts +38 -0
- package/dist/Registry.d.ts.map +1 -0
- package/dist/Registry.js +84 -0
- package/dist/Registry.js.map +1 -0
- package/dist/RegistryError.d.ts +4 -0
- package/dist/RegistryError.d.ts.map +1 -0
- package/dist/RegistryError.js +7 -0
- package/dist/RegistryError.js.map +1 -0
- package/dist/Term.d.ts +15 -0
- package/dist/Term.d.ts.map +1 -0
- package/dist/Term.js +22 -0
- package/dist/Term.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -3
- package/src/Registry.ts +108 -0
- package/src/RegistryError.ts +7 -0
- package/src/Term.ts +22 -0
- package/src/index.ts +3 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fougere contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fougere/registry
|
|
2
|
+
> One answer per name
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
`Sources` says which source type is `'sqlite'`, `Generators` which generator is `'ulid'`, `Protocols`
|
|
5
|
+
which adapter serves `'graphql'`. Each is a registry: a package answers a question by NAME, and a name
|
|
6
|
+
has one answer. Zero dependencies. (A question answered by TYPE — a class that extends a port — is
|
|
7
|
+
the other form of the same extension point, read off the class hierarchy at boot.)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add @fougere/registry
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## The rule
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const Generators = new Registry<() => string>('generator');
|
|
18
|
+
|
|
19
|
+
Generators.register('ulid', ulid); // the answer
|
|
20
|
+
Generators.register('ulid', ulid); // the same answer again: nothing happens
|
|
21
|
+
Generators.register('ulid', other); // refused — 'ulid' is answered twice for generator
|
|
22
|
+
Generators.replace('ulid', other); // said: standing in its place is the point
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
A host that re-reads a project in the same process re-runs the files that answer, and says so:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
await Registry.reloading(() => boot(), (registry, name) => log.debug(`${registry} '${name}' replaced`));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Inside that call, an answer replaces the one it had. The packages the host does not re-run keep
|
|
32
|
+
theirs, so a reload loses nothing.
|
|
33
|
+
|
|
34
|
+
## A term
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
export const ulid = Generators.term('ulid', () => …); // a name and its answer, as a value
|
|
38
|
+
ulid.register(); // registered where it is used
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A term is what a project's `vocabulary/` file exports: the statement lists it for the process
|
|
42
|
+
(`export const vocabulary = [ulid]` in `fronds.ts`), and the boot registers it. Imported for a value,
|
|
43
|
+
it is never dropped by a bundler the way a file imported only to register is.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Term } from './Term.js';
|
|
2
|
+
/** What a reload replaced — told to whoever reloaded, since this package holds no logger. */
|
|
3
|
+
export type Replaced = (registry: string, name: string) => void;
|
|
4
|
+
/**
|
|
5
|
+
* A question a package opens — `which source type is 'sqlite'?`, `which generator is 'ulid'?` — and
|
|
6
|
+
* the one answer each name has. A name answered twice is refused naming the registry, unless the same
|
|
7
|
+
* answer comes back or `replace` says standing in its place is the point.
|
|
8
|
+
*
|
|
9
|
+
* A host that re-reads a project in the same process — `fougere serve --watch` — re-runs the files
|
|
10
|
+
* that answer, and says so: inside `Registry.reloading`, an answer replaces the one it had.
|
|
11
|
+
* The packages it does not re-run keep theirs, so nothing is lost and nothing is noted.
|
|
12
|
+
*/
|
|
13
|
+
export declare class Registry<T> {
|
|
14
|
+
readonly label: string;
|
|
15
|
+
private readonly hint?;
|
|
16
|
+
private static replaced;
|
|
17
|
+
/** Every registry this process made — what a report of the extension points reads. */
|
|
18
|
+
private static readonly made;
|
|
19
|
+
private readonly held;
|
|
20
|
+
constructor(label: string, hint?: string | undefined, entries?: Iterable<readonly [string, T]>);
|
|
21
|
+
/** Every registry this process made, each by its label and the names it answers. */
|
|
22
|
+
static every(): {
|
|
23
|
+
label: string;
|
|
24
|
+
names: string[];
|
|
25
|
+
}[];
|
|
26
|
+
/** Runs `reload`, during which a second answer under a name replaces the first. */
|
|
27
|
+
static reloading<R>(reload: () => Promise<R>, replaced?: Replaced): Promise<R>;
|
|
28
|
+
register(name: string, value: T): T;
|
|
29
|
+
/** The answer as a value — exported from `vocabulary/`, registered by the frond that states it. */
|
|
30
|
+
term(name: string, value: T): Term<T>;
|
|
31
|
+
replace(name: string, value: T): T;
|
|
32
|
+
find(name: string): T | undefined;
|
|
33
|
+
resolve(name: string, path?: string): T;
|
|
34
|
+
get names(): string[];
|
|
35
|
+
get entries(): [string, T][];
|
|
36
|
+
get all(): T[];
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=Registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Registry.d.ts","sourceRoot":"","sources":["../src/Registry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,6FAA6F;AAC7F,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAEhE;;;;;;;;GAQG;AACH,qBAAa,QAAQ,CAAC,CAAC;IASnB,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;IATxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAuB;IAE9C,sFAAsF;IACtF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAgC;IAE5D,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;IAEtC,YACW,KAAK,EAAE,MAAM,EACL,IAAI,CAAC,EAAE,MAAM,YAAA,EAC9B,OAAO,CAAC,EAAE,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAIzC;IAED,oFAAoF;IACpF,MAAM,CAAC,KAAK,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAEnD;IAED,mFAAmF;IACnF,OAAa,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAmB,GAAG,OAAO,CAAC,CAAC,CAAC,CAQ7F;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAgBlC;IAED,mGAAmG;IACnG,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAEpC;IAED,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAQjC;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAEhC;IAED,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,CAAC,CAStC;IAED,IAAI,KAAK,IAAI,MAAM,EAAE,CAEpB;IAED,IAAI,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAE3B;IAED,IAAI,GAAG,IAAI,CAAC,EAAE,CAEb;CACF"}
|
package/dist/Registry.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { RegistryError } from './RegistryError.js';
|
|
2
|
+
import { Term } from './Term.js';
|
|
3
|
+
/**
|
|
4
|
+
* A question a package opens — `which source type is 'sqlite'?`, `which generator is 'ulid'?` — and
|
|
5
|
+
* the one answer each name has. A name answered twice is refused naming the registry, unless the same
|
|
6
|
+
* answer comes back or `replace` says standing in its place is the point.
|
|
7
|
+
*
|
|
8
|
+
* A host that re-reads a project in the same process — `fougere serve --watch` — re-runs the files
|
|
9
|
+
* that answer, and says so: inside `Registry.reloading`, an answer replaces the one it had.
|
|
10
|
+
* The packages it does not re-run keep theirs, so nothing is lost and nothing is noted.
|
|
11
|
+
*/
|
|
12
|
+
export class Registry {
|
|
13
|
+
label;
|
|
14
|
+
hint;
|
|
15
|
+
static replaced;
|
|
16
|
+
/** Every registry this process made — what a report of the extension points reads. */
|
|
17
|
+
static made = new Set();
|
|
18
|
+
held;
|
|
19
|
+
constructor(label, hint, entries) {
|
|
20
|
+
this.label = label;
|
|
21
|
+
this.hint = hint;
|
|
22
|
+
this.held = new Map(entries);
|
|
23
|
+
Registry.made.add(this);
|
|
24
|
+
}
|
|
25
|
+
/** Every registry this process made, each by its label and the names it answers. */
|
|
26
|
+
static every() {
|
|
27
|
+
return [...Registry.made].map((one) => ({ label: one.label, names: one.names }));
|
|
28
|
+
}
|
|
29
|
+
/** Runs `reload`, during which a second answer under a name replaces the first. */
|
|
30
|
+
static async reloading(reload, replaced = () => { }) {
|
|
31
|
+
const outer = Registry.replaced;
|
|
32
|
+
Registry.replaced = replaced;
|
|
33
|
+
try {
|
|
34
|
+
return await reload();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
Registry.replaced = outer;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
register(name, value) {
|
|
41
|
+
const held = this.held.get(name);
|
|
42
|
+
if (held !== undefined && held !== value) {
|
|
43
|
+
const replaced = Registry.replaced;
|
|
44
|
+
if (!replaced) {
|
|
45
|
+
throw new RegistryError(`'${name}' is answered twice for ${this.label} — a second answer would silently replace the first. `
|
|
46
|
+
+ 'Name one of them differently, or call replace() when standing in its place is the point.');
|
|
47
|
+
}
|
|
48
|
+
replaced(this.label, name);
|
|
49
|
+
}
|
|
50
|
+
this.held.set(name, value);
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/** The answer as a value — exported from `vocabulary/`, registered by the frond that states it. */
|
|
54
|
+
term(name, value) {
|
|
55
|
+
return new Term(this, name, value);
|
|
56
|
+
}
|
|
57
|
+
replace(name, value) {
|
|
58
|
+
if (!this.held.has(name)) {
|
|
59
|
+
throw new RegistryError(`'${name}' answers nothing for ${this.label} yet, so nothing can be replaced — use register().`);
|
|
60
|
+
}
|
|
61
|
+
this.held.set(name, value);
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
find(name) {
|
|
65
|
+
return this.held.get(name);
|
|
66
|
+
}
|
|
67
|
+
resolve(name, path) {
|
|
68
|
+
const found = this.find(name);
|
|
69
|
+
if (found !== undefined)
|
|
70
|
+
return found;
|
|
71
|
+
throw new RegistryError(`${path ? `${path}: ` : ''}Unknown ${this.label} '${name}'${this.hint ? ` — ${this.hint}` : ''}. `
|
|
72
|
+
+ `This process answers ${this.names.join(', ') || 'nothing yet'}.`);
|
|
73
|
+
}
|
|
74
|
+
get names() {
|
|
75
|
+
return [...this.held.keys()];
|
|
76
|
+
}
|
|
77
|
+
get entries() {
|
|
78
|
+
return [...this.held];
|
|
79
|
+
}
|
|
80
|
+
get all() {
|
|
81
|
+
return [...this.held.values()];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=Registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Registry.js","sourceRoot":"","sources":["../src/Registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistryError.d.ts","sourceRoot":"","sources":["../src/RegistryError.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAO,EAAE,MAAM,EAI1B;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegistryError.js","sourceRoot":"","sources":["../src/RegistryError.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF"}
|
package/dist/Term.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Registry } from './Registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* A name and its answer, held as a value — what a `vocabulary/` file exports, and what the frond
|
|
4
|
+
* stating it registers. A declaration still writes the NAME (`format: 'slug'`), which is what a card
|
|
5
|
+
* carries; the answer travels with the frond, so it is imported rather than loaded for its effect.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Term<T> {
|
|
8
|
+
readonly registry: Registry<T>;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly answer: T;
|
|
11
|
+
constructor(registry: Registry<T>, name: string, answer: T);
|
|
12
|
+
static is(value: unknown): value is Term<unknown>;
|
|
13
|
+
register(): void;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=Term.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Term.d.ts","sourceRoot":"","sources":["../src/Term.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;GAIG;AACH,qBAAa,IAAI,CAAC,CAAC;IAEf,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,MAAM,EAAE,CAAC;IAHpB,YACW,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EACrB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,CAAC,EAChB;IAEJ,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,CAEhD;IAED,QAAQ,IAAI,IAAI,CAEf;CACF"}
|
package/dist/Term.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A name and its answer, held as a value — what a `vocabulary/` file exports, and what the frond
|
|
3
|
+
* stating it registers. A declaration still writes the NAME (`format: 'slug'`), which is what a card
|
|
4
|
+
* carries; the answer travels with the frond, so it is imported rather than loaded for its effect.
|
|
5
|
+
*/
|
|
6
|
+
export class Term {
|
|
7
|
+
registry;
|
|
8
|
+
name;
|
|
9
|
+
answer;
|
|
10
|
+
constructor(registry, name, answer) {
|
|
11
|
+
this.registry = registry;
|
|
12
|
+
this.name = name;
|
|
13
|
+
this.answer = answer;
|
|
14
|
+
}
|
|
15
|
+
static is(value) {
|
|
16
|
+
return value instanceof Term;
|
|
17
|
+
}
|
|
18
|
+
register() {
|
|
19
|
+
this.registry.register(this.name, this.answer);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=Term.js.map
|
package/dist/Term.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Term.js","sourceRoot":"","sources":["../src/Term.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,IAAI;IAEJ,QAAQ;IACR,IAAI;IACJ,MAAM;IAHjB,YACW,QAAqB,EACrB,IAAY,EACZ,MAAS;wBAFT,QAAQ;oBACR,IAAI;sBACJ,MAAM;IACd,CAAC;IAEJ,MAAM,CAAC,EAAE,CAAC,KAAc;QACtB,OAAO,KAAK,YAAY,IAAI,CAAC;IAC/B,CAAC;IAED,QAAQ;QACN,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACjD,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAiB,MAAM,eAAe,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/registry",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.23.0-alpha.0",
|
|
4
|
+
"description": "Fougere's registry: one answer per name, refused when a second one arrives, and a reload that says it replaces. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fougere",
|
|
7
|
+
"typescript",
|
|
8
|
+
"registry",
|
|
9
|
+
"plugin"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/chok/fougere.git",
|
|
15
|
+
"directory": "packages/registry"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"vitest": "^4.1.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "rm -rf dist && tsc",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest",
|
|
41
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
42
|
+
}
|
|
6
43
|
}
|
package/src/Registry.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { RegistryError } from './RegistryError.js';
|
|
2
|
+
import { Term } from './Term.js';
|
|
3
|
+
|
|
4
|
+
/** What a reload replaced — told to whoever reloaded, since this package holds no logger. */
|
|
5
|
+
export type Replaced = (registry: string, name: string) => void;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A question a package opens — `which source type is 'sqlite'?`, `which generator is 'ulid'?` — and
|
|
9
|
+
* the one answer each name has. A name answered twice is refused naming the registry, unless the same
|
|
10
|
+
* answer comes back or `replace` says standing in its place is the point.
|
|
11
|
+
*
|
|
12
|
+
* A host that re-reads a project in the same process — `fougere serve --watch` — re-runs the files
|
|
13
|
+
* that answer, and says so: inside `Registry.reloading`, an answer replaces the one it had.
|
|
14
|
+
* The packages it does not re-run keep theirs, so nothing is lost and nothing is noted.
|
|
15
|
+
*/
|
|
16
|
+
export class Registry<T> {
|
|
17
|
+
private static replaced: Replaced | undefined;
|
|
18
|
+
|
|
19
|
+
/** Every registry this process made — what a report of the extension points reads. */
|
|
20
|
+
private static readonly made = new Set<Registry<unknown>>();
|
|
21
|
+
|
|
22
|
+
private readonly held: Map<string, T>;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
readonly label: string,
|
|
26
|
+
private readonly hint?: string,
|
|
27
|
+
entries?: Iterable<readonly [string, T]>,
|
|
28
|
+
) {
|
|
29
|
+
this.held = new Map(entries);
|
|
30
|
+
Registry.made.add(this as Registry<unknown>);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Every registry this process made, each by its label and the names it answers. */
|
|
34
|
+
static every(): { label: string; names: string[] }[] {
|
|
35
|
+
return [...Registry.made].map((one) => ({ label: one.label, names: one.names }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Runs `reload`, during which a second answer under a name replaces the first. */
|
|
39
|
+
static async reloading<R>(reload: () => Promise<R>, replaced: Replaced = () => {}): Promise<R> {
|
|
40
|
+
const outer = Registry.replaced;
|
|
41
|
+
Registry.replaced = replaced;
|
|
42
|
+
try {
|
|
43
|
+
return await reload();
|
|
44
|
+
} finally {
|
|
45
|
+
Registry.replaced = outer;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
register(name: string, value: T): T {
|
|
50
|
+
const held = this.held.get(name);
|
|
51
|
+
if (held !== undefined && held !== value) {
|
|
52
|
+
const replaced = Registry.replaced;
|
|
53
|
+
if (!replaced) {
|
|
54
|
+
throw new RegistryError(
|
|
55
|
+
`'${name}' is answered twice for ${this.label} — a second answer would silently replace the first. `
|
|
56
|
+
+ 'Name one of them differently, or call replace() when standing in its place is the point.',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
replaced(this.label, name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.held.set(name, value);
|
|
63
|
+
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The answer as a value — exported from `vocabulary/`, registered by the frond that states it. */
|
|
68
|
+
term(name: string, value: T): Term<T> {
|
|
69
|
+
return new Term(this, name, value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
replace(name: string, value: T): T {
|
|
73
|
+
if (!this.held.has(name)) {
|
|
74
|
+
throw new RegistryError(`'${name}' answers nothing for ${this.label} yet, so nothing can be replaced — use register().`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.held.set(name, value);
|
|
78
|
+
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
find(name: string): T | undefined {
|
|
83
|
+
return this.held.get(name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
resolve(name: string, path?: string): T {
|
|
87
|
+
const found = this.find(name);
|
|
88
|
+
|
|
89
|
+
if (found !== undefined) return found;
|
|
90
|
+
|
|
91
|
+
throw new RegistryError(
|
|
92
|
+
`${path ? `${path}: ` : ''}Unknown ${this.label} '${name}'${this.hint ? ` — ${this.hint}` : ''}. `
|
|
93
|
+
+ `This process answers ${this.names.join(', ') || 'nothing yet'}.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get names(): string[] {
|
|
98
|
+
return [...this.held.keys()];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get entries(): [string, T][] {
|
|
102
|
+
return [...this.held];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get all(): T[] {
|
|
106
|
+
return [...this.held.values()];
|
|
107
|
+
}
|
|
108
|
+
}
|
package/src/Term.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Registry } from './Registry.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A name and its answer, held as a value — what a `vocabulary/` file exports, and what the frond
|
|
5
|
+
* stating it registers. A declaration still writes the NAME (`format: 'slug'`), which is what a card
|
|
6
|
+
* carries; the answer travels with the frond, so it is imported rather than loaded for its effect.
|
|
7
|
+
*/
|
|
8
|
+
export class Term<T> {
|
|
9
|
+
constructor(
|
|
10
|
+
readonly registry: Registry<T>,
|
|
11
|
+
readonly name: string,
|
|
12
|
+
readonly answer: T,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
static is(value: unknown): value is Term<unknown> {
|
|
16
|
+
return value instanceof Term;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
register(): void {
|
|
20
|
+
this.registry.register(this.name, this.answer);
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
ADDED