@run0/jiki 0.1.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/dist/browser-bundle.d.ts +40 -0
- package/dist/builtins.d.ts +22 -0
- package/dist/code-transform.d.ts +7 -0
- package/dist/config/cdn.d.ts +13 -0
- package/dist/container.d.ts +101 -0
- package/dist/dev-server.d.ts +69 -0
- package/dist/errors.d.ts +19 -0
- package/dist/frameworks/code-transforms.d.ts +32 -0
- package/dist/frameworks/next-api-handler.d.ts +72 -0
- package/dist/frameworks/next-dev-server.d.ts +141 -0
- package/dist/frameworks/next-html-generator.d.ts +36 -0
- package/dist/frameworks/next-route-resolver.d.ts +19 -0
- package/dist/frameworks/next-shims.d.ts +78 -0
- package/dist/frameworks/remix-dev-server.d.ts +47 -0
- package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
- package/dist/frameworks/vite-dev-server.d.ts +50 -0
- package/dist/fs-errors.d.ts +36 -0
- package/dist/index.cjs +14916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.mjs +14898 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kernel.d.ts +48 -0
- package/dist/memfs.d.ts +144 -0
- package/dist/metrics.d.ts +78 -0
- package/dist/module-resolver.d.ts +60 -0
- package/dist/network-interceptor.d.ts +71 -0
- package/dist/npm/cache.d.ts +76 -0
- package/dist/npm/index.d.ts +60 -0
- package/dist/npm/lockfile-reader.d.ts +32 -0
- package/dist/npm/pnpm.d.ts +18 -0
- package/dist/npm/registry.d.ts +45 -0
- package/dist/npm/resolver.d.ts +39 -0
- package/dist/npm/sync-installer.d.ts +18 -0
- package/dist/npm/tarball.d.ts +4 -0
- package/dist/npm/workspaces.d.ts +46 -0
- package/dist/persistence.d.ts +94 -0
- package/dist/plugin.d.ts +156 -0
- package/dist/polyfills/assert.d.ts +30 -0
- package/dist/polyfills/child_process.d.ts +116 -0
- package/dist/polyfills/chokidar.d.ts +18 -0
- package/dist/polyfills/crypto.d.ts +49 -0
- package/dist/polyfills/events.d.ts +28 -0
- package/dist/polyfills/fs.d.ts +82 -0
- package/dist/polyfills/http.d.ts +147 -0
- package/dist/polyfills/module.d.ts +29 -0
- package/dist/polyfills/net.d.ts +53 -0
- package/dist/polyfills/os.d.ts +91 -0
- package/dist/polyfills/path.d.ts +96 -0
- package/dist/polyfills/perf_hooks.d.ts +21 -0
- package/dist/polyfills/process.d.ts +99 -0
- package/dist/polyfills/querystring.d.ts +15 -0
- package/dist/polyfills/readdirp.d.ts +18 -0
- package/dist/polyfills/readline.d.ts +32 -0
- package/dist/polyfills/stream.d.ts +106 -0
- package/dist/polyfills/stubs.d.ts +737 -0
- package/dist/polyfills/tty.d.ts +25 -0
- package/dist/polyfills/url.d.ts +41 -0
- package/dist/polyfills/util.d.ts +61 -0
- package/dist/polyfills/v8.d.ts +43 -0
- package/dist/polyfills/vm.d.ts +76 -0
- package/dist/polyfills/worker-threads.d.ts +77 -0
- package/dist/polyfills/ws.d.ts +32 -0
- package/dist/polyfills/zlib.d.ts +87 -0
- package/dist/runtime-helpers.d.ts +4 -0
- package/dist/runtime-interface.d.ts +39 -0
- package/dist/sandbox.d.ts +69 -0
- package/dist/server-bridge.d.ts +55 -0
- package/dist/shell-commands.d.ts +2 -0
- package/dist/shell.d.ts +101 -0
- package/dist/transpiler.d.ts +47 -0
- package/dist/type-checker.d.ts +57 -0
- package/dist/types/package-json.d.ts +17 -0
- package/dist/utils/binary-encoding.d.ts +4 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/safe-path.d.ts +6 -0
- package/dist/worker-runtime.d.ts +34 -0
- package/package.json +59 -0
- package/src/browser-bundle.ts +498 -0
- package/src/builtins.ts +222 -0
- package/src/code-transform.ts +183 -0
- package/src/config/cdn.ts +17 -0
- package/src/container.ts +343 -0
- package/src/dev-server.ts +322 -0
- package/src/errors.ts +604 -0
- package/src/frameworks/code-transforms.ts +667 -0
- package/src/frameworks/next-api-handler.ts +366 -0
- package/src/frameworks/next-dev-server.ts +1252 -0
- package/src/frameworks/next-html-generator.ts +585 -0
- package/src/frameworks/next-route-resolver.ts +521 -0
- package/src/frameworks/next-shims.ts +1084 -0
- package/src/frameworks/remix-dev-server.ts +163 -0
- package/src/frameworks/sveltekit-dev-server.ts +197 -0
- package/src/frameworks/vite-dev-server.ts +370 -0
- package/src/fs-errors.ts +118 -0
- package/src/index.ts +188 -0
- package/src/kernel.ts +381 -0
- package/src/memfs.ts +1006 -0
- package/src/metrics.ts +140 -0
- package/src/module-resolver.ts +511 -0
- package/src/network-interceptor.ts +143 -0
- package/src/npm/cache.ts +172 -0
- package/src/npm/index.ts +377 -0
- package/src/npm/lockfile-reader.ts +105 -0
- package/src/npm/pnpm.ts +108 -0
- package/src/npm/registry.ts +120 -0
- package/src/npm/resolver.ts +339 -0
- package/src/npm/sync-installer.ts +217 -0
- package/src/npm/tarball.ts +136 -0
- package/src/npm/workspaces.ts +255 -0
- package/src/persistence.ts +235 -0
- package/src/plugin.ts +293 -0
- package/src/polyfills/assert.ts +164 -0
- package/src/polyfills/child_process.ts +535 -0
- package/src/polyfills/chokidar.ts +52 -0
- package/src/polyfills/crypto.ts +433 -0
- package/src/polyfills/events.ts +178 -0
- package/src/polyfills/fs.ts +297 -0
- package/src/polyfills/http.ts +478 -0
- package/src/polyfills/module.ts +97 -0
- package/src/polyfills/net.ts +123 -0
- package/src/polyfills/os.ts +108 -0
- package/src/polyfills/path.ts +169 -0
- package/src/polyfills/perf_hooks.ts +30 -0
- package/src/polyfills/process.ts +349 -0
- package/src/polyfills/querystring.ts +66 -0
- package/src/polyfills/readdirp.ts +72 -0
- package/src/polyfills/readline.ts +80 -0
- package/src/polyfills/stream.ts +610 -0
- package/src/polyfills/stubs.ts +600 -0
- package/src/polyfills/tty.ts +43 -0
- package/src/polyfills/url.ts +97 -0
- package/src/polyfills/util.ts +173 -0
- package/src/polyfills/v8.ts +62 -0
- package/src/polyfills/vm.ts +111 -0
- package/src/polyfills/worker-threads.ts +189 -0
- package/src/polyfills/ws.ts +73 -0
- package/src/polyfills/zlib.ts +244 -0
- package/src/runtime-helpers.ts +83 -0
- package/src/runtime-interface.ts +46 -0
- package/src/sandbox.ts +178 -0
- package/src/server-bridge.ts +473 -0
- package/src/service-worker.ts +153 -0
- package/src/shell-commands.ts +708 -0
- package/src/shell.ts +795 -0
- package/src/transpiler.ts +282 -0
- package/src/type-checker.ts +241 -0
- package/src/types/package-json.ts +17 -0
- package/src/utils/binary-encoding.ts +38 -0
- package/src/utils/hash.ts +24 -0
- package/src/utils/safe-path.ts +38 -0
- package/src/worker-runtime.ts +42 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence layer for MemFS.
|
|
3
|
+
*
|
|
4
|
+
* Provides a `PersistenceAdapter` interface and an `IndexedDBAdapter`
|
|
5
|
+
* implementation that synchronises the in-memory filesystem to IndexedDB.
|
|
6
|
+
*
|
|
7
|
+
* Writes are fire-and-forget — they never block the synchronous VFS API.
|
|
8
|
+
* Mutations are batched and flushed via microtask debounce (~100 ms) so
|
|
9
|
+
* rapid consecutive writes result in a single IndexedDB transaction.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const adapter = new IndexedDBAdapter('my-project');
|
|
14
|
+
* const vfs = new MemFS({ persistence: adapter });
|
|
15
|
+
* await vfs.hydrate(); // load persisted state
|
|
16
|
+
* vfs.writeFileSync('/hello.txt', 'world'); // auto-persisted
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { FSNode } from "./memfs";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Public interface
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/** Serialised form of a single filesystem entry stored in the adapter. */
|
|
27
|
+
export interface PersistedEntry {
|
|
28
|
+
path: string;
|
|
29
|
+
type: "file" | "directory" | "symlink";
|
|
30
|
+
/** File content as Uint8Array. Only set for type === "file". */
|
|
31
|
+
content?: Uint8Array;
|
|
32
|
+
/** Symlink target. Only set for type === "symlink". */
|
|
33
|
+
target?: string;
|
|
34
|
+
mtime: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Backend-agnostic interface for persisting MemFS state.
|
|
39
|
+
* Implement this to plug in any storage backend (IndexedDB, OPFS,
|
|
40
|
+
* localStorage, etc.).
|
|
41
|
+
*/
|
|
42
|
+
export interface PersistenceAdapter {
|
|
43
|
+
/** Persist a single entry (upsert by path). */
|
|
44
|
+
save(entry: PersistedEntry): void;
|
|
45
|
+
/** Delete a persisted entry by path. */
|
|
46
|
+
delete(path: string): void;
|
|
47
|
+
/** Load all persisted entries. */
|
|
48
|
+
loadAll(): Promise<PersistedEntry[]>;
|
|
49
|
+
/** Delete all persisted entries. */
|
|
50
|
+
clear(): Promise<void>;
|
|
51
|
+
/** Flush any pending writes immediately. Returns when the flush completes. */
|
|
52
|
+
flush(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// IndexedDB adapter
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/** Options for the IndexedDB adapter. */
|
|
60
|
+
export interface IndexedDBAdapterOptions {
|
|
61
|
+
/** IndexedDB database name. Defaults to `"jiki-vfs"`. */
|
|
62
|
+
dbName?: string;
|
|
63
|
+
/** Object store name. Defaults to `"files"`. */
|
|
64
|
+
storeName?: string;
|
|
65
|
+
/** Batch flush interval in milliseconds. Defaults to 100. */
|
|
66
|
+
flushIntervalMs?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Persists MemFS entries to IndexedDB.
|
|
71
|
+
*
|
|
72
|
+
* Mutations are batched into a queue and flushed periodically (default
|
|
73
|
+
* every 100 ms) in a single readwrite transaction for performance.
|
|
74
|
+
*/
|
|
75
|
+
export class IndexedDBAdapter implements PersistenceAdapter {
|
|
76
|
+
private dbName: string;
|
|
77
|
+
private storeName: string;
|
|
78
|
+
private flushIntervalMs: number;
|
|
79
|
+
private db: IDBDatabase | null = null;
|
|
80
|
+
private queue: Array<
|
|
81
|
+
{ type: "save"; entry: PersistedEntry } | { type: "delete"; path: string }
|
|
82
|
+
> = [];
|
|
83
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
84
|
+
private flushPromise: Promise<void> | null = null;
|
|
85
|
+
|
|
86
|
+
constructor(options: IndexedDBAdapterOptions = {}) {
|
|
87
|
+
this.dbName = options.dbName ?? "jiki-vfs";
|
|
88
|
+
this.storeName = options.storeName ?? "files";
|
|
89
|
+
this.flushIntervalMs = options.flushIntervalMs ?? 100;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async openDB(): Promise<IDBDatabase> {
|
|
93
|
+
if (this.db) return this.db;
|
|
94
|
+
return new Promise<IDBDatabase>((resolve, reject) => {
|
|
95
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
96
|
+
request.onupgradeneeded = () => {
|
|
97
|
+
const db = request.result;
|
|
98
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
99
|
+
db.createObjectStore(this.storeName, { keyPath: "path" });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
request.onsuccess = () => {
|
|
103
|
+
this.db = request.result;
|
|
104
|
+
resolve(this.db);
|
|
105
|
+
};
|
|
106
|
+
request.onerror = () => reject(request.error);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
save(entry: PersistedEntry): void {
|
|
111
|
+
this.queue.push({ type: "save", entry });
|
|
112
|
+
this.scheduleFlush();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
delete(path: string): void {
|
|
116
|
+
this.queue.push({ type: "delete", path });
|
|
117
|
+
this.scheduleFlush();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async loadAll(): Promise<PersistedEntry[]> {
|
|
121
|
+
const db = await this.openDB();
|
|
122
|
+
return new Promise<PersistedEntry[]>((resolve, reject) => {
|
|
123
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
124
|
+
const store = tx.objectStore(this.storeName);
|
|
125
|
+
const request = store.getAll();
|
|
126
|
+
request.onsuccess = () => resolve(request.result as PersistedEntry[]);
|
|
127
|
+
request.onerror = () => reject(request.error);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async clear(): Promise<void> {
|
|
132
|
+
const db = await this.openDB();
|
|
133
|
+
return new Promise<void>((resolve, reject) => {
|
|
134
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
135
|
+
const store = tx.objectStore(this.storeName);
|
|
136
|
+
const request = store.clear();
|
|
137
|
+
request.onsuccess = () => resolve();
|
|
138
|
+
request.onerror = () => reject(request.error);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async flush(): Promise<void> {
|
|
143
|
+
if (this.flushTimer !== null) {
|
|
144
|
+
clearTimeout(this.flushTimer);
|
|
145
|
+
this.flushTimer = null;
|
|
146
|
+
}
|
|
147
|
+
if (this.queue.length === 0) return;
|
|
148
|
+
|
|
149
|
+
const batch = this.queue.splice(0);
|
|
150
|
+
const db = await this.openDB();
|
|
151
|
+
|
|
152
|
+
return new Promise<void>((resolve, reject) => {
|
|
153
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
154
|
+
const store = tx.objectStore(this.storeName);
|
|
155
|
+
|
|
156
|
+
for (const op of batch) {
|
|
157
|
+
if (op.type === "save") {
|
|
158
|
+
store.put(op.entry);
|
|
159
|
+
} else {
|
|
160
|
+
store.delete(op.path);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
tx.oncomplete = () => resolve();
|
|
165
|
+
tx.onerror = () => reject(tx.error);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private scheduleFlush(): void {
|
|
170
|
+
if (this.flushTimer !== null) return;
|
|
171
|
+
this.flushTimer = setTimeout(() => {
|
|
172
|
+
this.flushTimer = null;
|
|
173
|
+
this.flushPromise = this.flush().catch(err => {
|
|
174
|
+
console.error("[jiki:persistence] flush failed:", err);
|
|
175
|
+
});
|
|
176
|
+
}, this.flushIntervalMs);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// In-memory adapter (for testing and non-browser environments)
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* In-memory adapter that stores entries in a `Map`.
|
|
186
|
+
* Useful for tests and server-side usage where IndexedDB is unavailable.
|
|
187
|
+
*/
|
|
188
|
+
export class InMemoryAdapter implements PersistenceAdapter {
|
|
189
|
+
private store = new Map<string, PersistedEntry>();
|
|
190
|
+
|
|
191
|
+
save(entry: PersistedEntry): void {
|
|
192
|
+
this.store.set(entry.path, entry);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
delete(path: string): void {
|
|
196
|
+
this.store.delete(path);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async loadAll(): Promise<PersistedEntry[]> {
|
|
200
|
+
return Array.from(this.store.values());
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async clear(): Promise<void> {
|
|
204
|
+
this.store.clear();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async flush(): Promise<void> {
|
|
208
|
+
// No-op — writes are synchronous in memory.
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Return the number of stored entries (test helper). */
|
|
212
|
+
get size(): number {
|
|
213
|
+
return this.store.size;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Helper: convert FSNode ↔ PersistedEntry
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
/** Convert an FSNode at the given path to a PersistedEntry. */
|
|
222
|
+
export function nodeToEntry(path: string, node: FSNode): PersistedEntry {
|
|
223
|
+
const entry: PersistedEntry = {
|
|
224
|
+
path,
|
|
225
|
+
type: node.type,
|
|
226
|
+
mtime: node.mtime,
|
|
227
|
+
};
|
|
228
|
+
if (node.type === "file" && node.content) {
|
|
229
|
+
entry.content = node.content;
|
|
230
|
+
}
|
|
231
|
+
if (node.type === "symlink" && node.target) {
|
|
232
|
+
entry.target = node.target;
|
|
233
|
+
}
|
|
234
|
+
return entry;
|
|
235
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin system for jiki.
|
|
3
|
+
*
|
|
4
|
+
* Provides lifecycle hooks that let external code intercept and extend
|
|
5
|
+
* jiki's behaviour at key points: module resolution, module loading,
|
|
6
|
+
* code transformation, shell command registration, package installation,
|
|
7
|
+
* and container boot.
|
|
8
|
+
*
|
|
9
|
+
* The API mirrors esbuild's plugin conventions so it feels familiar to
|
|
10
|
+
* most JavaScript developers.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const myPlugin: JikiPlugin = {
|
|
15
|
+
* name: 'my-plugin',
|
|
16
|
+
* setup(hooks) {
|
|
17
|
+
* hooks.onResolve(/^virtual:/, (args) => ({
|
|
18
|
+
* path: `/virtual/${args.path.slice(8)}`,
|
|
19
|
+
* }));
|
|
20
|
+
* hooks.onLoad(/^\/virtual\//, (args) => ({
|
|
21
|
+
* contents: `module.exports = "hello from ${args.path}";`,
|
|
22
|
+
* }));
|
|
23
|
+
* },
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* const container = boot({ plugins: [myPlugin] });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Public types
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** Arguments passed to an `onResolve` callback. */
|
|
35
|
+
export interface OnResolveArgs {
|
|
36
|
+
/** The raw module specifier (e.g. `"virtual:config"` or `"./foo"`). */
|
|
37
|
+
path: string;
|
|
38
|
+
/** Directory the import originates from. */
|
|
39
|
+
resolveDir: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Return value from an `onResolve` callback. */
|
|
43
|
+
export interface OnResolveResult {
|
|
44
|
+
/** Resolved absolute path. Returning this skips the default resolver. */
|
|
45
|
+
path: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Arguments passed to an `onLoad` callback. */
|
|
49
|
+
export interface OnLoadArgs {
|
|
50
|
+
/** The fully-resolved module path. */
|
|
51
|
+
path: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Return value from an `onLoad` callback. */
|
|
55
|
+
export interface OnLoadResult {
|
|
56
|
+
/** Source code to use instead of reading from the VFS. */
|
|
57
|
+
contents: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Arguments passed to an `onTransform` callback. */
|
|
61
|
+
export interface OnTransformArgs {
|
|
62
|
+
/** The fully-resolved file path. */
|
|
63
|
+
path: string;
|
|
64
|
+
/** Source code *after* previous transforms in the pipeline. */
|
|
65
|
+
contents: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Return value from an `onTransform` callback. */
|
|
69
|
+
export interface OnTransformResult {
|
|
70
|
+
/** Transformed source code. */
|
|
71
|
+
contents: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Callback types for lifecycle hooks. */
|
|
75
|
+
export type OnResolveCallback = (
|
|
76
|
+
args: OnResolveArgs,
|
|
77
|
+
) => OnResolveResult | null | undefined | void;
|
|
78
|
+
export type OnLoadCallback = (
|
|
79
|
+
args: OnLoadArgs,
|
|
80
|
+
) => OnLoadResult | null | undefined | void;
|
|
81
|
+
export type OnTransformCallback = (
|
|
82
|
+
args: OnTransformArgs,
|
|
83
|
+
) => OnTransformResult | null | undefined | void;
|
|
84
|
+
export type OnInstallCallback = (packages: string[]) => void;
|
|
85
|
+
export type OnBootCallback = () => void;
|
|
86
|
+
|
|
87
|
+
import type { CommandHandler } from "./shell";
|
|
88
|
+
|
|
89
|
+
/** Hook registration API handed to {@link JikiPlugin.setup}. */
|
|
90
|
+
export interface PluginHooks {
|
|
91
|
+
/**
|
|
92
|
+
* Intercept module resolution. The first callback whose `filter` matches
|
|
93
|
+
* **and** returns a non-null result wins — later callbacks are skipped.
|
|
94
|
+
*/
|
|
95
|
+
onResolve(filter: RegExp, callback: OnResolveCallback): void;
|
|
96
|
+
/**
|
|
97
|
+
* Intercept module loading. The first callback whose `filter` matches
|
|
98
|
+
* **and** returns a non-null result wins — later callbacks are skipped.
|
|
99
|
+
*/
|
|
100
|
+
onLoad(filter: RegExp, callback: OnLoadCallback): void;
|
|
101
|
+
/**
|
|
102
|
+
* Intercept code transformation. Unlike resolve/load, this is a
|
|
103
|
+
* **pipeline**: every matching callback runs in registration order,
|
|
104
|
+
* each receiving the output of the previous one.
|
|
105
|
+
*/
|
|
106
|
+
onTransform(filter: RegExp, callback: OnTransformCallback): void;
|
|
107
|
+
/** Register a custom shell command. */
|
|
108
|
+
onCommand(name: string, handler: CommandHandler): void;
|
|
109
|
+
/** Called after packages are installed. */
|
|
110
|
+
onInstall(callback: OnInstallCallback): void;
|
|
111
|
+
/** Called after the container is fully initialised. */
|
|
112
|
+
onBoot(callback: OnBootCallback): void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A jiki plugin. */
|
|
116
|
+
export interface JikiPlugin {
|
|
117
|
+
/** Human-readable name (used in error messages and debugging). */
|
|
118
|
+
name: string;
|
|
119
|
+
/** Called once during container construction. Register hooks here. */
|
|
120
|
+
setup(hooks: PluginHooks): void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Internal registry
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
interface ResolveEntry {
|
|
128
|
+
filter: RegExp;
|
|
129
|
+
callback: OnResolveCallback;
|
|
130
|
+
plugin: string;
|
|
131
|
+
}
|
|
132
|
+
interface LoadEntry {
|
|
133
|
+
filter: RegExp;
|
|
134
|
+
callback: OnLoadCallback;
|
|
135
|
+
plugin: string;
|
|
136
|
+
}
|
|
137
|
+
interface TransformEntry {
|
|
138
|
+
filter: RegExp;
|
|
139
|
+
callback: OnTransformCallback;
|
|
140
|
+
plugin: string;
|
|
141
|
+
}
|
|
142
|
+
interface CommandEntry {
|
|
143
|
+
name: string;
|
|
144
|
+
handler: CommandHandler;
|
|
145
|
+
plugin: string;
|
|
146
|
+
}
|
|
147
|
+
interface InstallEntry {
|
|
148
|
+
callback: OnInstallCallback;
|
|
149
|
+
plugin: string;
|
|
150
|
+
}
|
|
151
|
+
interface BootEntry {
|
|
152
|
+
callback: OnBootCallback;
|
|
153
|
+
plugin: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Central registry that collects hooks from all plugins and exposes
|
|
158
|
+
* methods for the runtime to invoke them.
|
|
159
|
+
*
|
|
160
|
+
* Intentionally not a singleton — each {@link Container} gets its own
|
|
161
|
+
* `PluginRegistry` so plugins cannot leak between containers.
|
|
162
|
+
*/
|
|
163
|
+
export class PluginRegistry {
|
|
164
|
+
private resolveHooks: ResolveEntry[] = [];
|
|
165
|
+
private loadHooks: LoadEntry[] = [];
|
|
166
|
+
private transformHooks: TransformEntry[] = [];
|
|
167
|
+
private commandHooks: CommandEntry[] = [];
|
|
168
|
+
private installHooks: InstallEntry[] = [];
|
|
169
|
+
private bootHooks: BootEntry[] = [];
|
|
170
|
+
|
|
171
|
+
/** True if at least one plugin has been registered. */
|
|
172
|
+
get hasPlugins(): boolean {
|
|
173
|
+
return (
|
|
174
|
+
this.resolveHooks.length > 0 ||
|
|
175
|
+
this.loadHooks.length > 0 ||
|
|
176
|
+
this.transformHooks.length > 0 ||
|
|
177
|
+
this.commandHooks.length > 0 ||
|
|
178
|
+
this.installHooks.length > 0 ||
|
|
179
|
+
this.bootHooks.length > 0
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Number of registered resolve hooks. */
|
|
184
|
+
get resolveHookCount(): number {
|
|
185
|
+
return this.resolveHooks.length;
|
|
186
|
+
}
|
|
187
|
+
/** Number of registered load hooks. */
|
|
188
|
+
get loadHookCount(): number {
|
|
189
|
+
return this.loadHooks.length;
|
|
190
|
+
}
|
|
191
|
+
/** Number of registered transform hooks. */
|
|
192
|
+
get transformHookCount(): number {
|
|
193
|
+
return this.transformHooks.length;
|
|
194
|
+
}
|
|
195
|
+
/** Number of registered command hooks. */
|
|
196
|
+
get commandHookCount(): number {
|
|
197
|
+
return this.commandHooks.length;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// -- Registration ---------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
/** Register all hooks from a single plugin. */
|
|
203
|
+
register(plugin: JikiPlugin): void {
|
|
204
|
+
const hooks: PluginHooks = {
|
|
205
|
+
onResolve: (filter, callback) => {
|
|
206
|
+
this.resolveHooks.push({ filter, callback, plugin: plugin.name });
|
|
207
|
+
},
|
|
208
|
+
onLoad: (filter, callback) => {
|
|
209
|
+
this.loadHooks.push({ filter, callback, plugin: plugin.name });
|
|
210
|
+
},
|
|
211
|
+
onTransform: (filter, callback) => {
|
|
212
|
+
this.transformHooks.push({ filter, callback, plugin: plugin.name });
|
|
213
|
+
},
|
|
214
|
+
onCommand: (name, handler) => {
|
|
215
|
+
this.commandHooks.push({ name, handler, plugin: plugin.name });
|
|
216
|
+
},
|
|
217
|
+
onInstall: callback => {
|
|
218
|
+
this.installHooks.push({ callback, plugin: plugin.name });
|
|
219
|
+
},
|
|
220
|
+
onBoot: callback => {
|
|
221
|
+
this.bootHooks.push({ callback, plugin: plugin.name });
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
plugin.setup(hooks);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// -- Invocation -----------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Run resolve hooks. First matching callback that returns a result wins.
|
|
231
|
+
* Returns `null` if no plugin handled the specifier.
|
|
232
|
+
*/
|
|
233
|
+
runResolve(path: string, resolveDir: string): OnResolveResult | null {
|
|
234
|
+
for (const entry of this.resolveHooks) {
|
|
235
|
+
if (entry.filter.test(path)) {
|
|
236
|
+
const result = entry.callback({ path, resolveDir });
|
|
237
|
+
if (result && result.path) return result;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Run load hooks. First matching callback that returns a result wins.
|
|
245
|
+
* Returns `null` if no plugin provided contents.
|
|
246
|
+
*/
|
|
247
|
+
runLoad(path: string): OnLoadResult | null {
|
|
248
|
+
for (const entry of this.loadHooks) {
|
|
249
|
+
if (entry.filter.test(path)) {
|
|
250
|
+
const result = entry.callback({ path });
|
|
251
|
+
if (result && result.contents !== undefined) return result;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Run transform hooks as a pipeline.
|
|
259
|
+
* Every matching callback runs in order, each receiving the output of the
|
|
260
|
+
* previous one. Returns the final transformed source.
|
|
261
|
+
*/
|
|
262
|
+
runTransform(path: string, contents: string): string {
|
|
263
|
+
let current = contents;
|
|
264
|
+
for (const entry of this.transformHooks) {
|
|
265
|
+
if (entry.filter.test(path)) {
|
|
266
|
+
const result = entry.callback({ path, contents: current });
|
|
267
|
+
if (result && result.contents !== undefined) {
|
|
268
|
+
current = result.contents;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return current;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Return all command hooks so the shell can register them. */
|
|
276
|
+
getCommandHooks(): CommandEntry[] {
|
|
277
|
+
return this.commandHooks;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Notify all install hooks. */
|
|
281
|
+
runInstall(packages: string[]): void {
|
|
282
|
+
for (const entry of this.installHooks) {
|
|
283
|
+
entry.callback(packages);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Notify all boot hooks. */
|
|
288
|
+
runBoot(): void {
|
|
289
|
+
for (const entry of this.bootHooks) {
|
|
290
|
+
entry.callback();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "./util";
|
|
2
|
+
|
|
3
|
+
class AssertionError extends Error {
|
|
4
|
+
actual: unknown;
|
|
5
|
+
expected: unknown;
|
|
6
|
+
operator: string;
|
|
7
|
+
constructor(
|
|
8
|
+
message: string,
|
|
9
|
+
actual?: unknown,
|
|
10
|
+
expected?: unknown,
|
|
11
|
+
operator?: string,
|
|
12
|
+
) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "AssertionError";
|
|
15
|
+
this.actual = actual;
|
|
16
|
+
this.expected = expected;
|
|
17
|
+
this.operator = operator || "";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function assert(value: unknown, message?: string): asserts value {
|
|
22
|
+
if (!value)
|
|
23
|
+
throw new AssertionError(
|
|
24
|
+
message || `Expected truthy value, got ${value}`,
|
|
25
|
+
value,
|
|
26
|
+
true,
|
|
27
|
+
"==",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
assert.ok = assert;
|
|
32
|
+
assert.equal = (actual: unknown, expected: unknown, message?: string) => {
|
|
33
|
+
if (actual != expected)
|
|
34
|
+
throw new AssertionError(
|
|
35
|
+
message || `${actual} != ${expected}`,
|
|
36
|
+
actual,
|
|
37
|
+
expected,
|
|
38
|
+
"==",
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
assert.notEqual = (actual: unknown, expected: unknown, message?: string) => {
|
|
42
|
+
if (actual == expected)
|
|
43
|
+
throw new AssertionError(
|
|
44
|
+
message || `${actual} == ${expected}`,
|
|
45
|
+
actual,
|
|
46
|
+
expected,
|
|
47
|
+
"!=",
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
assert.strictEqual = (actual: unknown, expected: unknown, message?: string) => {
|
|
51
|
+
if (actual !== expected)
|
|
52
|
+
throw new AssertionError(
|
|
53
|
+
message || `${actual} !== ${expected}`,
|
|
54
|
+
actual,
|
|
55
|
+
expected,
|
|
56
|
+
"===",
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
assert.notStrictEqual = (
|
|
60
|
+
actual: unknown,
|
|
61
|
+
expected: unknown,
|
|
62
|
+
message?: string,
|
|
63
|
+
) => {
|
|
64
|
+
if (actual === expected)
|
|
65
|
+
throw new AssertionError(
|
|
66
|
+
message || `${actual} === ${expected}`,
|
|
67
|
+
actual,
|
|
68
|
+
expected,
|
|
69
|
+
"!==",
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
assert.deepEqual = (actual: unknown, expected: unknown, message?: string) => {
|
|
73
|
+
if (!isDeepStrictEqual(actual, expected))
|
|
74
|
+
throw new AssertionError(
|
|
75
|
+
message || "Values not deep equal",
|
|
76
|
+
actual,
|
|
77
|
+
expected,
|
|
78
|
+
"deepEqual",
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
assert.deepStrictEqual = assert.deepEqual;
|
|
82
|
+
assert.notDeepEqual = (
|
|
83
|
+
actual: unknown,
|
|
84
|
+
expected: unknown,
|
|
85
|
+
message?: string,
|
|
86
|
+
) => {
|
|
87
|
+
if (isDeepStrictEqual(actual, expected))
|
|
88
|
+
throw new AssertionError(
|
|
89
|
+
message || "Values are deep equal",
|
|
90
|
+
actual,
|
|
91
|
+
expected,
|
|
92
|
+
"notDeepEqual",
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
assert.notDeepStrictEqual = assert.notDeepEqual;
|
|
96
|
+
assert.throws = (
|
|
97
|
+
fn: () => void,
|
|
98
|
+
errorOrMessage?: unknown,
|
|
99
|
+
message?: string,
|
|
100
|
+
) => {
|
|
101
|
+
let threw = false;
|
|
102
|
+
try {
|
|
103
|
+
fn();
|
|
104
|
+
} catch {
|
|
105
|
+
threw = true;
|
|
106
|
+
}
|
|
107
|
+
if (!threw)
|
|
108
|
+
throw new AssertionError(
|
|
109
|
+
typeof errorOrMessage === "string"
|
|
110
|
+
? errorOrMessage
|
|
111
|
+
: message || "Expected function to throw",
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
assert.doesNotThrow = (fn: () => void, message?: string) => {
|
|
115
|
+
try {
|
|
116
|
+
fn();
|
|
117
|
+
} catch (e) {
|
|
118
|
+
throw new AssertionError(message || `Function threw: ${e}`);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
assert.rejects = async (
|
|
122
|
+
fn: (() => Promise<unknown>) | Promise<unknown>,
|
|
123
|
+
message?: string,
|
|
124
|
+
) => {
|
|
125
|
+
try {
|
|
126
|
+
await (typeof fn === "function" ? fn() : fn);
|
|
127
|
+
} catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
throw new AssertionError(
|
|
131
|
+
typeof message === "string" ? message : "Expected promise to reject",
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
assert.doesNotReject = async (
|
|
135
|
+
fn: (() => Promise<unknown>) | Promise<unknown>,
|
|
136
|
+
message?: string,
|
|
137
|
+
) => {
|
|
138
|
+
try {
|
|
139
|
+
await (typeof fn === "function" ? fn() : fn);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
throw new AssertionError(
|
|
142
|
+
typeof message === "string" ? message : `Promise rejected: ${e}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
assert.ifError = (value: unknown) => {
|
|
147
|
+
if (value) throw value;
|
|
148
|
+
};
|
|
149
|
+
assert.fail = (message?: string) => {
|
|
150
|
+
throw new AssertionError(message || "Failed");
|
|
151
|
+
};
|
|
152
|
+
assert.match = (string: string, regexp: RegExp, message?: string) => {
|
|
153
|
+
if (!regexp.test(string))
|
|
154
|
+
throw new AssertionError(message || `${string} does not match ${regexp}`);
|
|
155
|
+
};
|
|
156
|
+
assert.doesNotMatch = (string: string, regexp: RegExp, message?: string) => {
|
|
157
|
+
if (regexp.test(string))
|
|
158
|
+
throw new AssertionError(message || `${string} matches ${regexp}`);
|
|
159
|
+
};
|
|
160
|
+
assert.AssertionError = AssertionError;
|
|
161
|
+
assert.strict = assert;
|
|
162
|
+
|
|
163
|
+
export { AssertionError };
|
|
164
|
+
export default assert;
|