@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.
Files changed (152) hide show
  1. package/dist/browser-bundle.d.ts +40 -0
  2. package/dist/builtins.d.ts +22 -0
  3. package/dist/code-transform.d.ts +7 -0
  4. package/dist/config/cdn.d.ts +13 -0
  5. package/dist/container.d.ts +101 -0
  6. package/dist/dev-server.d.ts +69 -0
  7. package/dist/errors.d.ts +19 -0
  8. package/dist/frameworks/code-transforms.d.ts +32 -0
  9. package/dist/frameworks/next-api-handler.d.ts +72 -0
  10. package/dist/frameworks/next-dev-server.d.ts +141 -0
  11. package/dist/frameworks/next-html-generator.d.ts +36 -0
  12. package/dist/frameworks/next-route-resolver.d.ts +19 -0
  13. package/dist/frameworks/next-shims.d.ts +78 -0
  14. package/dist/frameworks/remix-dev-server.d.ts +47 -0
  15. package/dist/frameworks/sveltekit-dev-server.d.ts +43 -0
  16. package/dist/frameworks/vite-dev-server.d.ts +50 -0
  17. package/dist/fs-errors.d.ts +36 -0
  18. package/dist/index.cjs +14916 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +61 -0
  21. package/dist/index.mjs +14898 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/kernel.d.ts +48 -0
  24. package/dist/memfs.d.ts +144 -0
  25. package/dist/metrics.d.ts +78 -0
  26. package/dist/module-resolver.d.ts +60 -0
  27. package/dist/network-interceptor.d.ts +71 -0
  28. package/dist/npm/cache.d.ts +76 -0
  29. package/dist/npm/index.d.ts +60 -0
  30. package/dist/npm/lockfile-reader.d.ts +32 -0
  31. package/dist/npm/pnpm.d.ts +18 -0
  32. package/dist/npm/registry.d.ts +45 -0
  33. package/dist/npm/resolver.d.ts +39 -0
  34. package/dist/npm/sync-installer.d.ts +18 -0
  35. package/dist/npm/tarball.d.ts +4 -0
  36. package/dist/npm/workspaces.d.ts +46 -0
  37. package/dist/persistence.d.ts +94 -0
  38. package/dist/plugin.d.ts +156 -0
  39. package/dist/polyfills/assert.d.ts +30 -0
  40. package/dist/polyfills/child_process.d.ts +116 -0
  41. package/dist/polyfills/chokidar.d.ts +18 -0
  42. package/dist/polyfills/crypto.d.ts +49 -0
  43. package/dist/polyfills/events.d.ts +28 -0
  44. package/dist/polyfills/fs.d.ts +82 -0
  45. package/dist/polyfills/http.d.ts +147 -0
  46. package/dist/polyfills/module.d.ts +29 -0
  47. package/dist/polyfills/net.d.ts +53 -0
  48. package/dist/polyfills/os.d.ts +91 -0
  49. package/dist/polyfills/path.d.ts +96 -0
  50. package/dist/polyfills/perf_hooks.d.ts +21 -0
  51. package/dist/polyfills/process.d.ts +99 -0
  52. package/dist/polyfills/querystring.d.ts +15 -0
  53. package/dist/polyfills/readdirp.d.ts +18 -0
  54. package/dist/polyfills/readline.d.ts +32 -0
  55. package/dist/polyfills/stream.d.ts +106 -0
  56. package/dist/polyfills/stubs.d.ts +737 -0
  57. package/dist/polyfills/tty.d.ts +25 -0
  58. package/dist/polyfills/url.d.ts +41 -0
  59. package/dist/polyfills/util.d.ts +61 -0
  60. package/dist/polyfills/v8.d.ts +43 -0
  61. package/dist/polyfills/vm.d.ts +76 -0
  62. package/dist/polyfills/worker-threads.d.ts +77 -0
  63. package/dist/polyfills/ws.d.ts +32 -0
  64. package/dist/polyfills/zlib.d.ts +87 -0
  65. package/dist/runtime-helpers.d.ts +4 -0
  66. package/dist/runtime-interface.d.ts +39 -0
  67. package/dist/sandbox.d.ts +69 -0
  68. package/dist/server-bridge.d.ts +55 -0
  69. package/dist/shell-commands.d.ts +2 -0
  70. package/dist/shell.d.ts +101 -0
  71. package/dist/transpiler.d.ts +47 -0
  72. package/dist/type-checker.d.ts +57 -0
  73. package/dist/types/package-json.d.ts +17 -0
  74. package/dist/utils/binary-encoding.d.ts +4 -0
  75. package/dist/utils/hash.d.ts +6 -0
  76. package/dist/utils/safe-path.d.ts +6 -0
  77. package/dist/worker-runtime.d.ts +34 -0
  78. package/package.json +59 -0
  79. package/src/browser-bundle.ts +498 -0
  80. package/src/builtins.ts +222 -0
  81. package/src/code-transform.ts +183 -0
  82. package/src/config/cdn.ts +17 -0
  83. package/src/container.ts +343 -0
  84. package/src/dev-server.ts +322 -0
  85. package/src/errors.ts +604 -0
  86. package/src/frameworks/code-transforms.ts +667 -0
  87. package/src/frameworks/next-api-handler.ts +366 -0
  88. package/src/frameworks/next-dev-server.ts +1252 -0
  89. package/src/frameworks/next-html-generator.ts +585 -0
  90. package/src/frameworks/next-route-resolver.ts +521 -0
  91. package/src/frameworks/next-shims.ts +1084 -0
  92. package/src/frameworks/remix-dev-server.ts +163 -0
  93. package/src/frameworks/sveltekit-dev-server.ts +197 -0
  94. package/src/frameworks/vite-dev-server.ts +370 -0
  95. package/src/fs-errors.ts +118 -0
  96. package/src/index.ts +188 -0
  97. package/src/kernel.ts +381 -0
  98. package/src/memfs.ts +1006 -0
  99. package/src/metrics.ts +140 -0
  100. package/src/module-resolver.ts +511 -0
  101. package/src/network-interceptor.ts +143 -0
  102. package/src/npm/cache.ts +172 -0
  103. package/src/npm/index.ts +377 -0
  104. package/src/npm/lockfile-reader.ts +105 -0
  105. package/src/npm/pnpm.ts +108 -0
  106. package/src/npm/registry.ts +120 -0
  107. package/src/npm/resolver.ts +339 -0
  108. package/src/npm/sync-installer.ts +217 -0
  109. package/src/npm/tarball.ts +136 -0
  110. package/src/npm/workspaces.ts +255 -0
  111. package/src/persistence.ts +235 -0
  112. package/src/plugin.ts +293 -0
  113. package/src/polyfills/assert.ts +164 -0
  114. package/src/polyfills/child_process.ts +535 -0
  115. package/src/polyfills/chokidar.ts +52 -0
  116. package/src/polyfills/crypto.ts +433 -0
  117. package/src/polyfills/events.ts +178 -0
  118. package/src/polyfills/fs.ts +297 -0
  119. package/src/polyfills/http.ts +478 -0
  120. package/src/polyfills/module.ts +97 -0
  121. package/src/polyfills/net.ts +123 -0
  122. package/src/polyfills/os.ts +108 -0
  123. package/src/polyfills/path.ts +169 -0
  124. package/src/polyfills/perf_hooks.ts +30 -0
  125. package/src/polyfills/process.ts +349 -0
  126. package/src/polyfills/querystring.ts +66 -0
  127. package/src/polyfills/readdirp.ts +72 -0
  128. package/src/polyfills/readline.ts +80 -0
  129. package/src/polyfills/stream.ts +610 -0
  130. package/src/polyfills/stubs.ts +600 -0
  131. package/src/polyfills/tty.ts +43 -0
  132. package/src/polyfills/url.ts +97 -0
  133. package/src/polyfills/util.ts +173 -0
  134. package/src/polyfills/v8.ts +62 -0
  135. package/src/polyfills/vm.ts +111 -0
  136. package/src/polyfills/worker-threads.ts +189 -0
  137. package/src/polyfills/ws.ts +73 -0
  138. package/src/polyfills/zlib.ts +244 -0
  139. package/src/runtime-helpers.ts +83 -0
  140. package/src/runtime-interface.ts +46 -0
  141. package/src/sandbox.ts +178 -0
  142. package/src/server-bridge.ts +473 -0
  143. package/src/service-worker.ts +153 -0
  144. package/src/shell-commands.ts +708 -0
  145. package/src/shell.ts +795 -0
  146. package/src/transpiler.ts +282 -0
  147. package/src/type-checker.ts +241 -0
  148. package/src/types/package-json.ts +17 -0
  149. package/src/utils/binary-encoding.ts +38 -0
  150. package/src/utils/hash.ts +24 -0
  151. package/src/utils/safe-path.ts +38 -0
  152. 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;