@warlock.js/core 4.0.18 → 4.0.21

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 (52) hide show
  1. package/esm/cli/cli-commands.manager.d.ts.map +1 -1
  2. package/esm/cli/cli-commands.manager.js +5 -4
  3. package/esm/cli/cli-commands.manager.js.map +1 -1
  4. package/esm/config/config-loader.d.ts.map +1 -1
  5. package/esm/config/config-loader.js +3 -2
  6. package/esm/config/config-loader.js.map +1 -1
  7. package/esm/dev2-server/development-server.d.ts.map +1 -1
  8. package/esm/dev2-server/development-server.js +1 -4
  9. package/esm/dev2-server/development-server.js.map +1 -1
  10. package/esm/dev2-server/file-event-handler.d.ts +115 -18
  11. package/esm/dev2-server/file-event-handler.d.ts.map +1 -1
  12. package/esm/dev2-server/file-event-handler.js +177 -63
  13. package/esm/dev2-server/file-event-handler.js.map +1 -1
  14. package/esm/dev2-server/file-manager.d.ts +301 -52
  15. package/esm/dev2-server/file-manager.d.ts.map +1 -1
  16. package/esm/dev2-server/file-manager.js +409 -203
  17. package/esm/dev2-server/file-manager.js.map +1 -1
  18. package/esm/dev2-server/file-operations.d.ts +161 -23
  19. package/esm/dev2-server/file-operations.d.ts.map +1 -1
  20. package/esm/dev2-server/file-operations.js +217 -80
  21. package/esm/dev2-server/file-operations.js.map +1 -1
  22. package/esm/dev2-server/files-orchestrator.d.ts +35 -11
  23. package/esm/dev2-server/files-orchestrator.d.ts.map +1 -1
  24. package/esm/dev2-server/files-orchestrator.js +51 -63
  25. package/esm/dev2-server/files-orchestrator.js.map +1 -1
  26. package/esm/dev2-server/import-transformer.d.ts +6 -5
  27. package/esm/dev2-server/import-transformer.d.ts.map +1 -1
  28. package/esm/dev2-server/import-transformer.js +27 -39
  29. package/esm/dev2-server/import-transformer.js.map +1 -1
  30. package/esm/dev2-server/layer-executor.js +1 -1
  31. package/esm/dev2-server/parse-imports.d.ts.map +1 -1
  32. package/esm/dev2-server/parse-imports.js +1 -0
  33. package/esm/dev2-server/parse-imports.js.map +1 -1
  34. package/esm/dev2-server/tsconfig-manager.d.ts +1 -1
  35. package/esm/dev2-server/tsconfig-manager.d.ts.map +1 -1
  36. package/esm/dev2-server/tsconfig-manager.js +8 -3
  37. package/esm/dev2-server/tsconfig-manager.js.map +1 -1
  38. package/esm/dev2-server/type-generator.d.ts +10 -6
  39. package/esm/dev2-server/type-generator.d.ts.map +1 -1
  40. package/esm/dev2-server/type-generator.js +84 -75
  41. package/esm/dev2-server/type-generator.js.map +1 -1
  42. package/esm/dev2-server/types.d.ts +15 -1
  43. package/esm/dev2-server/types.d.ts.map +1 -1
  44. package/esm/http/errors/resource-not-found.error.d.ts +4 -0
  45. package/esm/http/errors/resource-not-found.error.d.ts.map +1 -1
  46. package/esm/http/errors/resource-not-found.error.js +9 -1
  47. package/esm/http/errors/resource-not-found.error.js.map +1 -1
  48. package/esm/http/middleware/inject-request-context.d.ts.map +1 -1
  49. package/esm/http/middleware/inject-request-context.js +8 -2
  50. package/esm/http/middleware/inject-request-context.js.map +1 -1
  51. package/esm/index.js +1 -1
  52. package/package.json +1 -1
@@ -1,75 +1,157 @@
1
- import events from'@mongez/events';import {getFileAsync,lastModifiedAsync,putFileAsync}from'@mongez/fs';import crypto from'crypto';import {pathToFileURL}from'url';import {DEV_SERVER_EVENTS}from'./events.js';import {transformImports}from'./import-transformer.js';import {parseImports}from'./parse-imports.js';import {Path}from'./path.js';import {transpileFile}from'./transpile-file.js';import {warlockCachePath}from'./utils.js';class FileManager {
1
+ import events from'@mongez/events';import {getFileAsync,lastModifiedAsync,putFileAsync}from'@mongez/fs';import crypto from'crypto';import {pathToFileURL}from'url';import {DEV_SERVER_EVENTS}from'./events.js';import {transformImports}from'./import-transformer.js';import {parseImports}from'./parse-imports.js';import {Path}from'./path.js';import {transpileFile}from'./transpile-file.js';import {warlockCachePath}from'./utils.js';/**
2
+ * FileManager - Manages the lifecycle of a single source file
3
+ *
4
+ * ## Lifecycle States
5
+ *
6
+ * ```
7
+ * idle → loading → parsed → transpiled → ready
8
+ * ↑ │
9
+ * └──────────── (file changed) ──────────┘
10
+ * ```
11
+ *
12
+ * ## Processing Pipeline
13
+ *
14
+ * All file processing flows through a unified pipeline:
15
+ * 1. **Load** - Read source from disk
16
+ * 2. **Hash** - Calculate content hash for change detection
17
+ * 3. **Parse** - Discover imports and dependencies
18
+ * 4. **Transpile** - Convert TypeScript to JavaScript
19
+ * 5. **Transform** - Rewrite import paths to cache locations
20
+ * 6. **Save** - Write transformed code to cache (ONCE)
21
+ *
22
+ * ## Usage Examples
23
+ *
24
+ * ```typescript
25
+ * // Standard processing (full pipeline)
26
+ * const file = new FileManager(absolutePath, filesMap, fileOps);
27
+ * await file.process();
28
+ *
29
+ * // Batch processing (parse first, complete later)
30
+ * await file.parse();
31
+ * // ... after dependencies are ready ...
32
+ * await file.complete();
33
+ *
34
+ * // Check for changes and reprocess if needed
35
+ * const changed = await file.process(); // returns false if unchanged
36
+ *
37
+ * // Force reprocessing
38
+ * await file.process({ force: true });
39
+ * ```
40
+ *
41
+ * @class FileManager
42
+ */
43
+ class FileManager {
2
44
  absolutePath;
3
45
  files;
4
46
  fileOperations;
5
47
  /**
6
- * Relative path to root directory
48
+ * Relative path from the project root directory
49
+ * Used as the primary identifier for files throughout the system
50
+ * @example "src/app/users/controllers/get-user.controller.ts"
7
51
  */
8
52
  relativePath = "";
9
53
  /**
10
- * Last modified timestamp
54
+ * Unix timestamp of the last modification time
55
+ * Used with hash for change detection
11
56
  */
12
57
  lastModified = 0;
13
58
  /**
14
- * Hash of the file content
59
+ * SHA-256 hash of the source content
60
+ * Primary mechanism for detecting file changes
15
61
  */
16
62
  hash = "";
17
63
  /**
18
- * Source code of the file
64
+ * Original TypeScript/JavaScript source code
65
+ * Loaded from disk during processing
19
66
  */
20
67
  source = "";
21
68
  /**
22
- * Transpiled code of the file
69
+ * Transpiled JavaScript code with transformed imports
70
+ * This is the final output that gets saved to cache and loaded at runtime
23
71
  */
24
72
  transpiled = "";
25
73
  /**
26
- * Dependencies of the file (relative paths)
74
+ * Set of relative paths that this file depends on (imports)
75
+ * Used to build the dependency graph for HMR invalidation
76
+ * @example Set(["src/app/users/models/user.model.ts", "src/config/database.ts"])
27
77
  */
28
78
  dependencies = new Set();
29
79
  /**
30
- * Import map: original import path -> resolved absolute path
31
- * Used for import transformation
80
+ * Map of original import specifiers to resolved absolute paths
81
+ * Key: the exact import string from source (e.g., "./user.model")
82
+ * Value: resolved absolute path (e.g., "D:/project/src/app/users/models/user.model.ts")
83
+ *
84
+ * Used during import transformation to rewrite paths to cache locations
32
85
  */
33
86
  importMap = new Map();
34
87
  /**
35
- * Dependents of the file
88
+ * Set of relative paths that depend on this file
89
+ * Populated from the dependency graph after initial processing
90
+ * Used to determine what needs reloading when this file changes
36
91
  */
37
92
  dependents = new Set();
38
93
  /**
39
- * Version of the file
94
+ * Version number incremented on each change
95
+ * Used for cache busting in dynamic imports
40
96
  */
41
97
  version = 0;
42
98
  /**
43
- * Type of the file
99
+ * Semantic type of the file based on its path/content
100
+ * Used to determine reload behavior and special handling
44
101
  */
45
102
  type;
46
103
  /**
47
- * Layer of the file
104
+ * Reload layer: HMR (hot module replacement) or FSR (full server restart)
105
+ * Determines how changes to this file are applied at runtime
48
106
  */
49
107
  layer;
50
108
  /**
51
- * Cache path of the file
109
+ * Path to the cached transpiled file (relative to .warlock/cache/)
110
+ * @example "src-app-users-controllers-get-user.controller.js"
52
111
  */
53
112
  cachePath = "";
54
113
  /**
55
- * File cleanup function
114
+ * Cleanup function called before the file is unloaded
115
+ * Set by module loader for files that export cleanup handlers
56
116
  */
57
117
  cleanup;
58
118
  /**
59
119
  * Whether imports have been transformed to cache paths
120
+ * Prevents double transformation and tracks processing state
60
121
  */
61
122
  importsTransformed = false;
62
123
  /**
63
124
  * Whether this file contains only type definitions (no runtime code)
64
- * Used to exclude from circular dependency detection
125
+ * Type-only files are excluded from circular dependency detection
65
126
  */
66
127
  isTypeOnlyFile = false;
67
128
  /**
68
- * File state
129
+ * Current processing state of the file
130
+ *
131
+ * - `idle`: Initial state, no processing started
132
+ * - `loading`: Reading source from disk
133
+ * - `parsed`: Source loaded and imports discovered
134
+ * - `transpiled`: TypeScript compiled to JavaScript
135
+ * - `ready`: Fully processed and available for use
136
+ * - `updating`: Being reprocessed after a change
137
+ * - `deleted`: File has been removed from disk
69
138
  */
70
139
  state = "idle";
71
140
  /**
72
- * Constructor
141
+ * Creates a new FileManager instance
142
+ *
143
+ * @param absolutePath - Full filesystem path to the source file
144
+ * @param files - Map of all tracked files (for import resolution)
145
+ * @param fileOperations - FileOperations instance (for adding missing dependencies)
146
+ *
147
+ * @example
148
+ * ```typescript
149
+ * const fileManager = new FileManager(
150
+ * "D:/project/src/app/users/controllers/get-user.controller.ts",
151
+ * filesMap,
152
+ * fileOperations
153
+ * );
154
+ * ```
73
155
  */
74
156
  constructor(absolutePath, files, fileOperations) {
75
157
  this.absolutePath = absolutePath;
@@ -77,26 +159,46 @@ import events from'@mongez/events';import {getFileAsync,lastModifiedAsync,putFil
77
159
  this.fileOperations = fileOperations;
78
160
  }
79
161
  /**
80
- * Initialize the file manager
81
- * @param fileManifest Optional manifest data from previous build
82
- * @param filesMap Optional map of all FileManager instances (for import transformation)
162
+ * Initialize the file manager from disk or manifest cache
163
+ *
164
+ * This is the primary entry point for file initialization.
165
+ * If manifest data is provided, it will attempt to use cached data
166
+ * and only reprocess if the file has changed.
167
+ *
168
+ * @param fileManifest - Optional cached manifest data from previous build
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * // Fresh initialization (no cache)
173
+ * await fileManager.init();
174
+ *
175
+ * // Initialize with cached manifest data
176
+ * await fileManager.init(manifestEntry);
177
+ * ```
83
178
  */
84
179
  async init(fileManifest) {
85
- this.state = "loading";
86
- // No manifest = fresh file, load from disk
87
- if (!fileManifest) {
88
- await this.loadFromDisk();
89
- // Import transformation will happen later in FilesOrchestrator.transformAllImports()
90
- // after all files are processed and available in the files map
91
- return;
180
+ // Set up basic paths
181
+ this.relativePath = Path.toRelative(this.absolutePath);
182
+ this.cachePath = this.relativePath.replace(/\//g, "-").replace(/\.(ts|tsx)$/, ".js");
183
+ this.detectFileTypeAndLayer();
184
+ if (fileManifest) {
185
+ await this.initFromManifest(fileManifest);
186
+ }
187
+ else {
188
+ // Fresh file - run full processing pipeline
189
+ await this.process();
92
190
  }
93
- // Manifest exists = check if file changed since last build
94
- await this.loadFromManifest(fileManifest);
95
- // Import transformation will happen later in FilesOrchestrator.transformAllImports()
96
- // for files that were reprocessed (importsTransformed = false)
97
191
  }
98
192
  /**
99
- * Get cached file path ready for dynamic import
193
+ * Get the cache path as a file:// URL for dynamic import
194
+ * Includes cache busting query parameter based on version
195
+ *
196
+ * @returns File URL ready for dynamic import, or empty string if no cache
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * const module = await import(fileManager.cachePathUrl);
201
+ * ```
100
202
  */
101
203
  get cachePathUrl() {
102
204
  if (!this.cachePath)
@@ -104,101 +206,130 @@ import events from'@mongez/events';import {getFileAsync,lastModifiedAsync,putFil
104
206
  return pathToFileURL(warlockCachePath(this.cachePath)).href;
105
207
  }
106
208
  /**
107
- * Load file with manifest data (check if changed)
209
+ * Process the file through the unified pipeline
210
+ *
211
+ * This is the core method that handles all file processing.
212
+ * It implements a single-pass pipeline that:
213
+ * 1. Loads source from disk
214
+ * 2. Checks if content changed (skip if unchanged)
215
+ * 3. Parses imports to discover dependencies
216
+ * 4. Ensures all dependencies exist
217
+ * 5. Transpiles TypeScript to JavaScript
218
+ * 6. Transforms imports to cache paths
219
+ * 7. Saves to cache (ONCE - no duplicate writes)
220
+ *
221
+ * @param options - Processing options
222
+ * @param options.force - Force reprocessing even if unchanged
223
+ * @param options.transformImports - Whether to transform import paths (default: true)
224
+ * @param options.saveToCache - Whether to save to cache file (default: true)
225
+ *
226
+ * @returns True if file was processed, false if unchanged and skipped
227
+ *
228
+ * @example
229
+ * ```typescript
230
+ * // Normal processing
231
+ * const changed = await fileManager.process();
232
+ *
233
+ * // Force reprocess
234
+ * await fileManager.process({ force: true });
235
+ *
236
+ * // Batch mode: transform imports later
237
+ * await fileManager.process({ transformImports: false });
238
+ * ```
108
239
  */
109
- async loadFromManifest(fileManifest) {
110
- // Set basic properties from manifest
111
- this.relativePath = fileManifest.relativePath || Path.toRelative(this.absolutePath);
112
- this.version = fileManifest.version || 0;
113
- this.type = fileManifest.type;
114
- this.layer = fileManifest.layer;
115
- this.cachePath =
116
- fileManifest.cachePath || this.relativePath.replace(/\//g, "-").replace(/\.(ts|tsx)$/, ".js");
117
- // Check if file still exists
240
+ async process(options = {}) {
241
+ const { force = false, transformImports: shouldTransform = true, saveToCache = true } = options;
242
+ // Ensure paths are initialized (in case process() is called directly)
243
+ if (!this.relativePath) {
244
+ this.relativePath = Path.toRelative(this.absolutePath);
245
+ }
246
+ if (!this.cachePath) {
247
+ this.cachePath = this.relativePath.replace(/\//g, "-").replace(/\.(ts|tsx)$/, ".js");
248
+ }
249
+ if (!this.type) {
250
+ this.detectFileTypeAndLayer();
251
+ }
252
+ this.state = "loading";
253
+ // Step 1: Load source from disk
254
+ let newSource;
118
255
  try {
119
- this.source = await getFileAsync(this.absolutePath);
256
+ newSource = await getFileAsync(this.absolutePath);
120
257
  }
121
258
  catch (error) {
122
259
  this.state = "deleted";
123
- return;
260
+ return false;
124
261
  }
125
- // Calculate current hash
126
- const currentHash = crypto.createHash("sha256").update(this.source).digest("hex");
127
- const currentLastModified = (await lastModifiedAsync(this.absolutePath)).getTime();
128
- // Compare with manifest data
129
- const hasChanged = currentHash !== fileManifest.hash;
130
- if (hasChanged) {
131
- // File changed - reprocess it
132
- this.hash = currentHash;
133
- this.lastModified = currentLastModified;
134
- this.version++;
135
- await this.processFile();
262
+ // Step 2: Calculate hash and check for changes
263
+ const newHash = crypto.createHash("sha256").update(newSource).digest("hex");
264
+ if (!force && newHash === this.hash && this.transpiled && this.importsTransformed) {
265
+ // File unchanged and already processed
266
+ this.state = "ready";
267
+ return false;
268
+ }
269
+ // Update source and metadata
270
+ this.source = newSource;
271
+ this.hash = newHash;
272
+ this.lastModified = (await lastModifiedAsync(this.absolutePath)).getTime();
273
+ this.version++;
274
+ // Step 3: Parse imports to discover dependencies
275
+ this.state = "parsed";
276
+ this.importMap = await parseImports(this.source, this.absolutePath);
277
+ this.dependencies = new Set(Array.from(this.importMap.values()).map((absPath) => Path.toRelative(absPath)));
278
+ // Note: Dependency existence is handled at orchestrator level, not here
279
+ // This keeps FM focused on single-file processing
280
+ // Step 4: Transpile TypeScript to JavaScript
281
+ this.state = "transpiled";
282
+ this.transpiled = await transpileFile(this);
283
+ // Step 5: Transform imports to cache paths
284
+ if (shouldTransform && this.dependencies.size > 0) {
285
+ this.transpiled = transformImports(this);
286
+ this.importsTransformed = true;
136
287
  }
137
288
  else {
138
- // File unchanged - load from cache
139
- this.hash = fileManifest.hash;
140
- this.lastModified = fileManifest.lastModified;
141
- this.dependencies = new Set(fileManifest.dependencies || []);
142
- this.dependents = new Set(fileManifest.dependents || []);
143
- // Load cached transpiled code
144
- try {
145
- this.transpiled = await getFileAsync(warlockCachePath(this.cachePath));
146
- // Cached files already have transformed imports
147
- this.importsTransformed = true;
148
- }
149
- catch (error) {
150
- // Cache missing - retranspile
151
- await this.processFile();
152
- // Will need import transformation
153
- this.importsTransformed = false;
154
- }
289
+ this.importsTransformed = false;
155
290
  }
291
+ // Step 6: Save to cache (ONCE - after all transformations)
292
+ if (saveToCache) {
293
+ await putFileAsync(warlockCachePath(this.cachePath), this.transpiled);
294
+ }
295
+ // Step 7: Mark as ready
156
296
  this.state = "ready";
157
297
  events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);
298
+ return true;
158
299
  }
159
300
  /**
160
- * Load file from disk (fresh, no manifest)
161
- */
162
- async loadFromDisk() {
163
- this.source = await getFileAsync(this.absolutePath);
164
- this.hash = crypto.createHash("sha256").update(this.source).digest("hex");
165
- this.relativePath = Path.toRelative(this.absolutePath);
166
- this.lastModified = (await lastModifiedAsync(this.absolutePath)).getTime();
167
- this.version = 0;
168
- this.detectFileTypeAndLayer();
169
- // Generate cache path (replace / with - and change extension to .js)
170
- this.cachePath = this.relativePath.replace(/\//g, "-").replace(/\.(ts|tsx)$/, ".js");
171
- await this.processFile();
172
- this.state = "ready";
173
- events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);
174
- }
175
- /**
176
- * Process file: parse imports first, then transpile
177
- */
178
- async processFile() {
179
- // STEP 1: Parse imports from source (must be first to get dependencies)
180
- const importMap = await parseImports(this.source, this.absolutePath);
181
- // Store dependencies as relative paths
182
- const importsRelativePaths = Array.from(importMap.values()).map((absPath) => Path.toRelative(absPath));
183
- this.dependencies = new Set(importsRelativePaths);
184
- // Store import map for later use in import transformation
185
- this.importMap = importMap;
186
- const missingImports = Array.from(this.dependencies.values()).filter((relativePath) => !this.files.has(relativePath));
187
- if (missingImports.length > 0) {
188
- // if missing, try to find them first in their locations
189
- await Promise.all(missingImports.map((relativePath) => this.fileOperations.addFile(relativePath)));
190
- }
191
- // STEP 2: Transpile source code
192
- this.transpiled = await transpileFile(this);
193
- // STEP 3: Save transpiled code to cache
194
- await putFileAsync(warlockCachePath(this.cachePath), this.transpiled);
195
- }
196
- /**
197
- * Phase 1: Parse the file to discover dependencies
198
- * Does NOT write cache yet - used for batch file processing
199
- * to determine processing order
301
+ * Parse the file to discover dependencies (Phase 1 of batch processing)
302
+ *
303
+ * This method only performs the first half of processing:
304
+ * - Loads source from disk
305
+ * - Calculates hash
306
+ * - Parses imports to discover dependencies
307
+ * - Sets up file metadata
308
+ *
309
+ * Use this for batch file operations where you need to know
310
+ * dependencies before deciding processing order.
311
+ *
312
+ * **Important**: After calling parse(), you must call complete()
313
+ * to finish processing and make the file usable.
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * // Batch processing pattern
318
+ * const files = await Promise.all(
319
+ * paths.map(async (path) => {
320
+ * const file = new FileManager(path, filesMap, fileOps);
321
+ * await file.parse();
322
+ * return file;
323
+ * })
324
+ * );
325
+ *
326
+ * // Order by dependencies, then complete
327
+ * for (const file of orderedFiles) {
328
+ * await file.complete();
329
+ * }
330
+ * ```
200
331
  */
201
- async parseOnly() {
332
+ async parse() {
202
333
  this.state = "loading";
203
334
  // Load source
204
335
  this.source = await getFileAsync(this.absolutePath);
@@ -206,40 +337,164 @@ import events from'@mongez/events';import {getFileAsync,lastModifiedAsync,putFil
206
337
  this.relativePath = Path.toRelative(this.absolutePath);
207
338
  this.lastModified = (await lastModifiedAsync(this.absolutePath)).getTime();
208
339
  this.version = 0;
340
+ // Set up cache path
341
+ this.cachePath = this.relativePath.replace(/\//g, "-").replace(/\.(ts|tsx)$/, ".js");
209
342
  // Detect type and layer
210
343
  this.detectFileTypeAndLayer();
211
- this.cachePath = this.relativePath.replace(/\//g, "-").replace(/\.(ts|tsx)$/, ".js");
212
344
  // Parse imports to discover dependencies
213
- const importMap = await parseImports(this.source, this.absolutePath);
214
- this.dependencies = new Set(Array.from(importMap.values()).map((absPath) => Path.toRelative(absPath)));
215
- this.importMap = importMap;
345
+ this.importMap = await parseImports(this.source, this.absolutePath);
346
+ this.dependencies = new Set(Array.from(this.importMap.values()).map((absPath) => Path.toRelative(absPath)));
216
347
  this.state = "parsed";
217
348
  }
218
349
  /**
219
- * Phase 2: Complete processing (transpile, transform, write cache)
220
- * Called after dependencies are ready
350
+ * Complete file processing after parse() (Phase 2 of batch processing)
351
+ *
352
+ * This method completes the processing pipeline:
353
+ * - Ensures dependencies exist
354
+ * - Transpiles TypeScript to JavaScript
355
+ * - Transforms imports to cache paths
356
+ * - Saves to cache
357
+ *
358
+ * **Important**: This must be called after parse() to finish processing.
359
+ * The file is not usable until complete() has been called.
360
+ *
361
+ * @example
362
+ * ```typescript
363
+ * await file.parse();
364
+ * // ... after dependencies are ready ...
365
+ * await file.complete();
366
+ * // File is now ready for use
367
+ * ```
221
368
  */
222
- async finalize() {
223
- // Re-parse imports to ensure importMap is populated correctly
224
- // (During parseOnly(), some batch files may not have existed on disk yet)
225
- const importMap = await parseImports(this.source, this.absolutePath);
226
- this.dependencies = new Set(Array.from(importMap.values()).map((absPath) => Path.toRelative(absPath)));
227
- this.importMap = importMap;
228
- // Check and add any missing dependencies first
229
- const missingImports = Array.from(this.dependencies.values()).filter((relativePath) => !this.files.has(relativePath));
230
- if (missingImports.length > 0) {
231
- await Promise.all(missingImports.map((relativePath) => this.fileOperations.addFile(relativePath)));
369
+ async complete() {
370
+ // Ensure we're in the right state
371
+ if (this.state !== "parsed") {
372
+ throw new Error(`Cannot complete file in state "${this.state}". Call parse() first.`);
232
373
  }
374
+ // Note: Dependency existence is handled at orchestrator/batch level
375
+ // by the time complete() is called, dependencies should already exist
233
376
  // Transpile
377
+ this.state = "transpiled";
234
378
  this.transpiled = await transpileFile(this);
235
- this.importsTransformed = false;
236
- // Transform imports to cache paths
237
- await this.transformImports();
379
+ // Transform imports
380
+ if (this.dependencies.size > 0) {
381
+ this.transpiled = transformImports(this);
382
+ }
383
+ this.importsTransformed = true;
384
+ // Save to cache (ONCE)
385
+ await putFileAsync(warlockCachePath(this.cachePath), this.transpiled);
386
+ // Mark as ready
238
387
  this.state = "ready";
239
388
  events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);
240
389
  }
241
390
  /**
242
- * Detect file type and layer based on path
391
+ * Update the file after a change during development
392
+ *
393
+ * This is a convenience method that delegates to process().
394
+ * It checks if the file has actually changed and only reprocesses if needed.
395
+ *
396
+ * @returns True if file was reprocessed, false if unchanged
397
+ *
398
+ * @example
399
+ * ```typescript
400
+ * // Called by file watcher
401
+ * const changed = await fileManager.update();
402
+ * if (changed) {
403
+ * console.log("File was updated");
404
+ * }
405
+ * ```
406
+ */
407
+ async update() {
408
+ return this.process();
409
+ }
410
+ /**
411
+ * Force reprocess the file regardless of hash match
412
+ *
413
+ * Use this when:
414
+ * - A dependency that was previously missing is now available
415
+ * - Import transformation needs to be redone
416
+ * - Cache is corrupted or missing
417
+ *
418
+ * @example
419
+ * ```typescript
420
+ * // After a missing dependency is added
421
+ * await fileManager.forceReprocess();
422
+ * ```
423
+ */
424
+ async forceReprocess() {
425
+ await this.process({ force: true });
426
+ }
427
+ /**
428
+ * Initialize from cached manifest data
429
+ *
430
+ * Attempts to use cached data from a previous build.
431
+ * If the file has changed (hash mismatch) or cache is missing,
432
+ * falls back to full processing.
433
+ *
434
+ * @param fileManifest - Cached manifest entry
435
+ * @internal
436
+ */
437
+ async initFromManifest(fileManifest) {
438
+ // Apply manifest properties
439
+ this.version = fileManifest.version || 0;
440
+ this.type = fileManifest.type;
441
+ this.layer = fileManifest.layer;
442
+ this.cachePath =
443
+ fileManifest.cachePath || this.relativePath.replace(/\//g, "-").replace(/\.(ts|tsx)$/, ".js");
444
+ // Load source and check for changes
445
+ this.state = "loading";
446
+ try {
447
+ this.source = await getFileAsync(this.absolutePath);
448
+ }
449
+ catch (error) {
450
+ this.state = "deleted";
451
+ return;
452
+ }
453
+ const currentHash = crypto.createHash("sha256").update(this.source).digest("hex");
454
+ const hasChanged = currentHash !== fileManifest.hash;
455
+ if (hasChanged) {
456
+ // File changed - full reprocess
457
+ this.hash = currentHash;
458
+ this.lastModified = (await lastModifiedAsync(this.absolutePath)).getTime();
459
+ this.version++;
460
+ await this.process({ force: true });
461
+ }
462
+ else {
463
+ // File unchanged - load from cache
464
+ this.hash = fileManifest.hash;
465
+ this.lastModified = fileManifest.lastModified;
466
+ this.dependencies = new Set(fileManifest.dependencies || []);
467
+ this.dependents = new Set(fileManifest.dependents || []);
468
+ try {
469
+ this.transpiled = await getFileAsync(warlockCachePath(this.cachePath));
470
+ this.importsTransformed = true;
471
+ this.state = "ready";
472
+ events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);
473
+ }
474
+ catch (error) {
475
+ // Cache missing - reprocess
476
+ await this.process({ force: true });
477
+ }
478
+ }
479
+ }
480
+ /**
481
+ * Detect the file type and reload layer based on path patterns
482
+ *
483
+ * File types determine special handling:
484
+ * - `main`: Application entry point
485
+ * - `config`: Configuration files
486
+ * - `route`: Route definitions
487
+ * - `controller`: HTTP controllers
488
+ * - `service`: Business logic services
489
+ * - `model`: Database models
490
+ * - `event`: Event handlers
491
+ * - `other`: Everything else
492
+ *
493
+ * Layers determine reload behavior:
494
+ * - `HMR`: Hot module replacement (instant reload)
495
+ * - `FSR`: Full server restart (required for some changes)
496
+ *
497
+ * @internal
243
498
  */
244
499
  detectFileTypeAndLayer() {
245
500
  // Main entry files
@@ -248,7 +503,7 @@ import events from'@mongez/events';import {getFileAsync,lastModifiedAsync,putFil
248
503
  this.layer = "HMR";
249
504
  return;
250
505
  }
251
- // Config files - FSR but handled specially (reload config + restart affected connectors)
506
+ // Config files
252
507
  if (this.relativePath.startsWith("src/config/")) {
253
508
  this.type = "config";
254
509
  this.layer = "HMR";
@@ -257,7 +512,7 @@ import events from'@mongez/events';import {getFileAsync,lastModifiedAsync,putFil
257
512
  // Routes files
258
513
  if (this.relativePath.endsWith("routes.ts") || this.relativePath.endsWith("routes.tsx")) {
259
514
  this.type = "route";
260
- this.layer = "HMR"; // For now FSR, will be HMR with wildcard routing
515
+ this.layer = "HMR";
261
516
  return;
262
517
  }
263
518
  // Event files
@@ -289,73 +544,24 @@ import events from'@mongez/events';import {getFileAsync,lastModifiedAsync,putFil
289
544
  this.layer = "HMR";
290
545
  }
291
546
  /**
292
- * Update file when changed during development
293
- */
294
- async update() {
295
- const newSource = await getFileAsync(this.absolutePath);
296
- // No change in content
297
- if (newSource.trim() === this.source.trim()) {
298
- return false;
299
- }
300
- this.state = "updating";
301
- this.source = newSource;
302
- this.version++;
303
- // Update hash and last modified
304
- this.hash = crypto.createHash("sha256").update(this.source).digest("hex");
305
- this.lastModified = (await lastModifiedAsync(this.absolutePath)).getTime();
306
- // Reprocess file
307
- await this.processFile();
308
- // Transform imports
309
- if (this.dependencies.size > 0) {
310
- this.importsTransformed = false;
311
- await this.transformImports();
312
- }
313
- this.state = "ready";
314
- return true;
315
- }
316
- /**
317
- * Force reprocess file even if source hasn't changed
318
- * Useful when dependencies become available (e.g., missing file is added back)
319
- */
320
- async forceReprocess() {
321
- this.state = "updating";
322
- this.version++;
323
- // Reprocess file (re-parse imports, retranspile, transform imports)
324
- await this.processFile();
325
- // Transform imports
326
- if (this.dependencies.size > 0) {
327
- this.importsTransformed = false;
328
- await this.transformImports();
329
- }
330
- this.state = "ready";
331
- }
332
- /**
333
- * Transform imports in transpiled code to use cache paths
334
- */
335
- async transformImports() {
336
- if (this.importsTransformed) {
337
- return; // Already transformed
338
- }
339
- try {
340
- // Transform imports in transpiled code
341
- const transformedCode = transformImports(this, this.files);
342
- // Update the transpiled code
343
- this.transpiled = transformedCode;
344
- // Save the transformed code back to cache
345
- await putFileAsync(warlockCachePath(this.cachePath), transformedCode);
346
- // Mark as transformed
347
- this.importsTransformed = true;
348
- }
349
- catch (error) {
350
- throw error;
351
- }
352
- }
353
- /**
354
- * Export file data as manifest entry
355
- * Note: source and transpiled code are NOT stored in manifest
356
- * - Source code is in the original file
357
- * - Transpiled code is in .warlock/cache/
358
- * - Hash is used to detect changes
547
+ * Export file data as a manifest entry for caching
548
+ *
549
+ * The manifest stores metadata about each file to enable:
550
+ * - Fast startup by skipping unchanged files
551
+ * - Dependency tracking across restarts
552
+ * - Cache validation via hash comparison
553
+ *
554
+ * Note: Source code and transpiled output are NOT stored in manifest.
555
+ * - Source is always read from disk
556
+ * - Transpiled code is stored in .warlock/cache/
557
+ *
558
+ * @returns Manifest entry for this file
559
+ *
560
+ * @example
561
+ * ```typescript
562
+ * const entry = fileManager.toManifest();
563
+ * manifest.setFile(fileManager.relativePath, entry);
564
+ * ```
359
565
  */
360
566
  toManifest() {
361
567
  return {