@auraindustry/aurajs 0.0.1

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.
@@ -0,0 +1,698 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ statSync,
8
+ writeFileSync,
9
+ readdirSync,
10
+ watch,
11
+ } from 'node:fs';
12
+ import { dirname, join, normalize, relative, resolve, sep } from 'node:path';
13
+
14
+ export const BUNDLE_ERROR_CODES = {
15
+ MISSING_ENTRY: 'AURA_MOD_001',
16
+ MODULE_NOT_FOUND: 'AURA_MOD_002',
17
+ UNSUPPORTED_SPECIFIER: 'AURA_MOD_003',
18
+ PATH_ESCAPE: 'AURA_MOD_004',
19
+ SYNTAX_ERROR: 'AURA_MOD_005',
20
+ CIRCULAR_DEP: 'AURA_MOD_006',
21
+ DYNAMIC_IMPORT: 'AURA_MOD_007',
22
+ };
23
+
24
+ class BundleError extends Error {
25
+ constructor(code, message, details = {}) {
26
+ super(message);
27
+ this.name = 'BundleError';
28
+ this.code = code;
29
+ this.details = details;
30
+ }
31
+ }
32
+
33
+ export function isBundleError(error) {
34
+ return error instanceof BundleError;
35
+ }
36
+
37
+ export function formatBundleError(error, projectRoot = process.cwd()) {
38
+ if (!isBundleError(error)) {
39
+ return String(error?.message || error || 'Unknown error');
40
+ }
41
+
42
+ const parts = [`${error.code}: ${error.message}`];
43
+ const details = error.details || {};
44
+
45
+ if (details.file) {
46
+ const rel = relPath(projectRoot, details.file);
47
+ if (details.line) {
48
+ parts.push(`at ${rel}:${details.line}`);
49
+ } else {
50
+ parts.push(`at ${rel}`);
51
+ }
52
+ }
53
+
54
+ if (details.importer || details.specifier) {
55
+ parts.push(
56
+ `import: ${details.specifier || '<unknown>'} (from ${
57
+ details.importer ? relPath(projectRoot, details.importer) : '<unknown>'
58
+ })`,
59
+ );
60
+ }
61
+
62
+ if (Array.isArray(details.candidates) && details.candidates.length > 0) {
63
+ parts.push('candidates:');
64
+ for (const candidate of details.candidates) {
65
+ parts.push(` - ${candidate}`);
66
+ }
67
+ }
68
+
69
+ if (details.cycle) {
70
+ const rendered = details.cycle.map((entry) => relPath(projectRoot, entry)).join(' -> ');
71
+ parts.push(`cycle: ${rendered}`);
72
+ }
73
+
74
+ return parts.join('\n');
75
+ }
76
+
77
+ export function createIncrementalBundler(options = {}) {
78
+ const projectRoot = resolve(options.projectRoot || process.cwd());
79
+ const entryFile = options.entryFile || null;
80
+ const cache = {
81
+ files: new Map(),
82
+ watchers: [],
83
+ poller: null,
84
+ pollFingerprint: '',
85
+ };
86
+
87
+ return {
88
+ build(buildOptions = {}) {
89
+ return bundleProject({
90
+ projectRoot,
91
+ mode: buildOptions.mode || 'dev',
92
+ cache,
93
+ outFile: buildOptions.outFile,
94
+ entryFile: buildOptions.entryFile || entryFile || undefined,
95
+ });
96
+ },
97
+ watch(onChange, onError) {
98
+ return startSourceWatch(projectRoot, cache, onChange, onError);
99
+ },
100
+ dispose() {
101
+ stopSourceWatch(cache);
102
+ },
103
+ };
104
+ }
105
+
106
+ export function bundleProject(options = {}) {
107
+ const startedAt = Date.now();
108
+ const projectRoot = resolve(options.projectRoot || process.cwd());
109
+ const mode = options.mode || 'build';
110
+ const cache = options.cache || { files: new Map() };
111
+
112
+ const srcDir = resolve(projectRoot, 'src');
113
+ const entryFile = options.entryFile
114
+ ? resolve(projectRoot, options.entryFile)
115
+ : resolve(srcDir, 'main.js');
116
+
117
+ if (!existsSync(entryFile)) {
118
+ const display = options.entryFile ? options.entryFile : 'src/main.js';
119
+ throw new BundleError(
120
+ BUNDLE_ERROR_CODES.MISSING_ENTRY,
121
+ `Missing entrypoint ${display}.`,
122
+ { file: entryFile },
123
+ );
124
+ }
125
+
126
+ const entryRel = relative(srcDir, entryFile);
127
+ if (entryRel.startsWith('..')) {
128
+ throw new BundleError(
129
+ BUNDLE_ERROR_CODES.PATH_ESCAPE,
130
+ 'Entrypoint must be inside src/.',
131
+ { file: entryFile },
132
+ );
133
+ }
134
+
135
+ const graph = buildModuleGraph({ srcDir, entryFile, cache });
136
+ const bundleText = emitBundle(graph);
137
+ const bundleHash = sha1(bundleText);
138
+
139
+ const defaultOut =
140
+ mode === 'dev'
141
+ ? resolve(projectRoot, '.aura/dev/game.bundle.js')
142
+ : mode === 'test'
143
+ ? resolve(projectRoot, '.aura/test/test.bundle.js')
144
+ : resolve(projectRoot, 'build/js/game.bundle.js');
145
+
146
+ const outFile = resolve(options.outFile || defaultOut);
147
+ mkdirSync(dirname(outFile), { recursive: true });
148
+ writeFileSync(outFile, bundleText, 'utf8');
149
+
150
+ return {
151
+ mode,
152
+ outFile,
153
+ moduleCount: graph.order.length,
154
+ entryId: graph.entryId,
155
+ hash: bundleHash,
156
+ elapsedMs: Date.now() - startedAt,
157
+ graph,
158
+ };
159
+ }
160
+
161
+ function buildModuleGraph(ctx) {
162
+ const state = new Map();
163
+ const modules = new Map();
164
+ const order = [];
165
+
166
+ visit(ctx.entryFile, []);
167
+
168
+ return {
169
+ srcDir: ctx.srcDir,
170
+ entryFile: ctx.entryFile,
171
+ entryId: moduleId(ctx.srcDir, ctx.entryFile),
172
+ modules,
173
+ order,
174
+ };
175
+
176
+ function visit(filePath, stack) {
177
+ const currentState = state.get(filePath) || 0;
178
+ if (currentState === 2) return;
179
+
180
+ if (currentState === 1) {
181
+ const cycleStart = stack.indexOf(filePath);
182
+ const cycle = cycleStart >= 0 ? [...stack.slice(cycleStart), filePath] : [...stack, filePath];
183
+ throw new BundleError(BUNDLE_ERROR_CODES.CIRCULAR_DEP, 'Circular dependency detected.', {
184
+ file: filePath,
185
+ cycle,
186
+ });
187
+ }
188
+
189
+ state.set(filePath, 1);
190
+
191
+ const parsed = readParsedModule(ctx, filePath);
192
+ const dependencies = [];
193
+
194
+ for (const entry of parsed.imports) {
195
+ const resolved = resolveSpecifier(ctx.srcDir, filePath, entry);
196
+ dependencies.push({ ...entry, resolved });
197
+ visit(resolved, [...stack, filePath]);
198
+ }
199
+
200
+ modules.set(filePath, {
201
+ id: moduleId(ctx.srcDir, filePath),
202
+ filePath,
203
+ code: parsed.code,
204
+ imports: dependencies,
205
+ });
206
+
207
+ state.set(filePath, 2);
208
+ order.push(filePath);
209
+ }
210
+ }
211
+
212
+ function emitBundle(graph) {
213
+ const lines = [];
214
+ lines.push('// AuraJS bundle v1');
215
+ lines.push('// Generated by AuraJS bundler (AS-043 semantics, AS-004 module rules)');
216
+ lines.push('(function(){');
217
+ lines.push(' const __auraModules = Object.create(null);');
218
+ lines.push(' const __auraCache = Object.create(null);');
219
+ lines.push('');
220
+
221
+ for (const filePath of graph.order) {
222
+ const moduleMeta = graph.modules.get(filePath);
223
+ const transformed = transformModule(moduleMeta, graph.srcDir);
224
+
225
+ lines.push(` __auraModules[${JSON.stringify(moduleMeta.id)}] = function(__module, __exports, __require){`);
226
+ for (const line of transformed.split('\n')) {
227
+ lines.push(` ${line}`);
228
+ }
229
+ lines.push(' };');
230
+ lines.push('');
231
+ }
232
+
233
+ lines.push(' function __require(id){');
234
+ lines.push(' if (__auraCache[id]) return __auraCache[id].exports;');
235
+ lines.push(' const factory = __auraModules[id];');
236
+ lines.push(' if (!factory) throw new Error(`AURA_RUNTIME_MISSING_MODULE: ${id}`);');
237
+ lines.push(' const module = { exports: Object.create(null) };');
238
+ lines.push(' __auraCache[id] = module;');
239
+ lines.push(' factory(module, module.exports, __require);');
240
+ lines.push(' return module.exports;');
241
+ lines.push(' }');
242
+ lines.push('');
243
+ lines.push(` __require(${JSON.stringify(graph.entryId)});`);
244
+ lines.push('})();');
245
+ lines.push('');
246
+
247
+ return lines.join('\n');
248
+ }
249
+
250
+ function transformModule(moduleMeta, srcDir) {
251
+ const importByLine = new Map();
252
+ for (const item of moduleMeta.imports) {
253
+ importByLine.set(item.line, item);
254
+ }
255
+
256
+ const out = [];
257
+ const exportAssignments = [];
258
+ const sourceLines = moduleMeta.code.split(/\r?\n/);
259
+ let importCounter = 0;
260
+
261
+ for (let idx = 0; idx < sourceLines.length; idx += 1) {
262
+ const source = sourceLines[idx];
263
+ const lineNo = idx + 1;
264
+
265
+ if (importByLine.has(lineNo)) {
266
+ const item = importByLine.get(lineNo);
267
+ const depId = moduleId(srcDir, item.resolved);
268
+ const transformedImport = renderImportReplacement(item.clause, depId, importCounter);
269
+ importCounter += 1;
270
+ out.push(...transformedImport);
271
+ continue;
272
+ }
273
+
274
+ const transformedExport = transformExportLine(source, lineNo, moduleMeta.filePath, exportAssignments);
275
+ out.push(...transformedExport);
276
+ }
277
+
278
+ if (exportAssignments.length > 0) {
279
+ out.push('');
280
+ out.push('// exports');
281
+ for (const line of exportAssignments) {
282
+ out.push(line);
283
+ }
284
+ }
285
+
286
+ return out.join('\n');
287
+ }
288
+
289
+ function transformExportLine(sourceLine, lineNo, filePath, exportAssignments) {
290
+ const trimmed = sourceLine.trim();
291
+ if (!trimmed.startsWith('export ')) {
292
+ return [sourceLine];
293
+ }
294
+
295
+ const leading = sourceLine.slice(0, sourceLine.indexOf('export'));
296
+
297
+ const defaultExpr = trimmed.match(/^export\s+default\s+(.+);?$/);
298
+ if (defaultExpr) {
299
+ return [`${leading}__exports.default = ${defaultExpr[1]};`];
300
+ }
301
+
302
+ const varExport = trimmed.match(/^export\s+(const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\b(.*)$/);
303
+ if (varExport) {
304
+ const declaration = `${leading}${varExport[1]} ${varExport[2]}${varExport[3]}`;
305
+ exportAssignments.push(`${leading}__exports.${varExport[2]} = ${varExport[2]};`);
306
+ return [declaration];
307
+ }
308
+
309
+ const fnExport = trimmed.match(/^export\s+function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/);
310
+ if (fnExport) {
311
+ const rewritten = sourceLine.replace('export ', '');
312
+ exportAssignments.push(`${leading}__exports.${fnExport[1]} = ${fnExport[1]};`);
313
+ return [rewritten];
314
+ }
315
+
316
+ const classExport = trimmed.match(/^export\s+class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/);
317
+ if (classExport) {
318
+ const rewritten = sourceLine.replace('export ', '');
319
+ exportAssignments.push(`${leading}__exports.${classExport[1]} = ${classExport[1]};`);
320
+ return [rewritten];
321
+ }
322
+
323
+ const namedExport = trimmed.match(/^export\s*\{([^}]+)\}\s*;?$/);
324
+ if (namedExport) {
325
+ const specifiers = namedExport[1]
326
+ .split(',')
327
+ .map((entry) => entry.trim())
328
+ .filter(Boolean);
329
+
330
+ const emitted = [];
331
+ for (const spec of specifiers) {
332
+ const alias = spec.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
333
+ if (alias) {
334
+ emitted.push(`${leading}__exports.${alias[2]} = ${alias[1]};`);
335
+ } else {
336
+ emitted.push(`${leading}__exports.${spec} = ${spec};`);
337
+ }
338
+ }
339
+ return emitted;
340
+ }
341
+
342
+ if (trimmed.startsWith('export *')) {
343
+ throw new BundleError(
344
+ BUNDLE_ERROR_CODES.SYNTAX_ERROR,
345
+ 'Unsupported export syntax. export * forms are not supported in v1.',
346
+ { file: filePath, line: lineNo },
347
+ );
348
+ }
349
+
350
+ throw new BundleError(
351
+ BUNDLE_ERROR_CODES.SYNTAX_ERROR,
352
+ 'Unsupported export syntax. Use named exports or export default expressions.',
353
+ { file: filePath, line: lineNo },
354
+ );
355
+ }
356
+
357
+ function renderImportReplacement(clause, depId, importCounter) {
358
+ if (!clause) {
359
+ return [`__require(${JSON.stringify(depId)});`];
360
+ }
361
+
362
+ const normalized = clause.trim();
363
+ if (!normalized) {
364
+ return [`__require(${JSON.stringify(depId)});`];
365
+ }
366
+
367
+ if (normalized.startsWith('{')) {
368
+ return [
369
+ `const ${rewriteNamedImports(normalized)} = __require(${JSON.stringify(depId)});`,
370
+ ];
371
+ }
372
+
373
+ if (normalized.startsWith('* as ')) {
374
+ const ns = normalized.slice(5).trim();
375
+ return [`const ${ns} = __require(${JSON.stringify(depId)});`];
376
+ }
377
+
378
+ if (normalized.includes(',')) {
379
+ const [head, tail] = normalized.split(',', 2);
380
+ const defaultName = head.trim();
381
+ const rest = tail.trim();
382
+ const tmp = `__import_${importCounter}`;
383
+ const lines = [`const ${tmp} = __require(${JSON.stringify(depId)});`];
384
+
385
+ if (defaultName) {
386
+ lines.push(`const { default: ${defaultName} } = ${tmp};`);
387
+ }
388
+
389
+ if (rest.startsWith('{')) {
390
+ lines.push(`const ${rewriteNamedImports(rest)} = ${tmp};`);
391
+ } else if (rest.startsWith('* as ')) {
392
+ lines.push(`const ${rest.slice(5).trim()} = ${tmp};`);
393
+ } else {
394
+ throw new BundleError(
395
+ BUNDLE_ERROR_CODES.SYNTAX_ERROR,
396
+ 'Unsupported mixed import syntax.',
397
+ {},
398
+ );
399
+ }
400
+
401
+ return lines;
402
+ }
403
+
404
+ return [`const { default: ${normalized} } = __require(${JSON.stringify(depId)});`];
405
+ }
406
+
407
+ function rewriteNamedImports(namedClause) {
408
+ const inner = namedClause.trim().replace(/^\{/, '').replace(/\}$/, '');
409
+ const specifiers = inner
410
+ .split(',')
411
+ .map((part) => part.trim())
412
+ .filter(Boolean)
413
+ .map((part) => {
414
+ const alias = part.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
415
+ if (alias) {
416
+ return `${alias[1]}: ${alias[2]}`;
417
+ }
418
+ return part;
419
+ });
420
+
421
+ return `{ ${specifiers.join(', ')} }`;
422
+ }
423
+
424
+ function readParsedModule(ctx, filePath) {
425
+ const content = readFileSync(filePath, 'utf8');
426
+ const hash = sha1(content);
427
+ const cached = ctx.cache.files.get(filePath);
428
+
429
+ if (cached && cached.hash === hash) {
430
+ return cached;
431
+ }
432
+
433
+ validateSyntax(content, filePath);
434
+
435
+ const imports = scanImports(content, filePath);
436
+ const parsed = { hash, code: content, imports };
437
+ ctx.cache.files.set(filePath, parsed);
438
+ return parsed;
439
+ }
440
+
441
+ function scanImports(code, filePath) {
442
+ if (hasDynamicImport(code)) {
443
+ const info = locateToken(code, 'import(');
444
+ throw new BundleError(
445
+ BUNDLE_ERROR_CODES.DYNAMIC_IMPORT,
446
+ 'Dynamic import() is not supported in AuraJS v1.',
447
+ {
448
+ file: filePath,
449
+ line: info.line,
450
+ column: info.column,
451
+ },
452
+ );
453
+ }
454
+
455
+ const imports = [];
456
+ const lines = code.split(/\r?\n/);
457
+
458
+ for (let idx = 0; idx < lines.length; idx += 1) {
459
+ const line = lines[idx];
460
+ const trimmed = line.trim();
461
+
462
+ if (!trimmed.startsWith('import ')) {
463
+ continue;
464
+ }
465
+
466
+ const lineNo = idx + 1;
467
+ const sideEffect = trimmed.match(/^import\s+['"]([^'"\n]+)['"]\s*;?$/);
468
+ if (sideEffect) {
469
+ imports.push({
470
+ line: lineNo,
471
+ column: line.indexOf('import') + 1,
472
+ clause: null,
473
+ specifier: sideEffect[1],
474
+ });
475
+ continue;
476
+ }
477
+
478
+ const fromImport = trimmed.match(/^import\s+(.+)\s+from\s+['"]([^'"\n]+)['"]\s*;?$/);
479
+ if (fromImport) {
480
+ imports.push({
481
+ line: lineNo,
482
+ column: line.indexOf('import') + 1,
483
+ clause: fromImport[1].trim(),
484
+ specifier: fromImport[2],
485
+ });
486
+ continue;
487
+ }
488
+
489
+ throw new BundleError(
490
+ BUNDLE_ERROR_CODES.SYNTAX_ERROR,
491
+ 'Unsupported import syntax. Use single-line static imports.',
492
+ { file: filePath, line: lineNo },
493
+ );
494
+ }
495
+
496
+ return imports;
497
+ }
498
+
499
+ function resolveSpecifier(srcDir, importer, importEntry) {
500
+ const specifier = importEntry.specifier;
501
+
502
+ if (!(specifier.startsWith('./') || specifier.startsWith('../'))) {
503
+ throw new BundleError(
504
+ BUNDLE_ERROR_CODES.UNSUPPORTED_SPECIFIER,
505
+ `Unsupported module specifier "${specifier}". Use relative imports only.`,
506
+ {
507
+ file: importer,
508
+ importer,
509
+ specifier,
510
+ line: importEntry.line,
511
+ },
512
+ );
513
+ }
514
+
515
+ const base = resolve(dirname(importer), specifier);
516
+ const candidates = [
517
+ normalize(base),
518
+ normalize(`${base}.js`),
519
+ normalize(`${base}.mjs`),
520
+ normalize(join(base, 'index.js')),
521
+ normalize(join(base, 'index.mjs')),
522
+ ];
523
+
524
+ for (const candidate of candidates) {
525
+ ensureWithinSource(srcDir, candidate, importer, importEntry);
526
+ if (existsSync(candidate) && statSync(candidate).isFile()) {
527
+ return candidate;
528
+ }
529
+ }
530
+
531
+ throw new BundleError(
532
+ BUNDLE_ERROR_CODES.MODULE_NOT_FOUND,
533
+ `Module not found for specifier "${specifier}".`,
534
+ {
535
+ file: importer,
536
+ importer,
537
+ specifier,
538
+ line: importEntry.line,
539
+ candidates: candidates.map((item) => relPath(srcDir, item)),
540
+ },
541
+ );
542
+ }
543
+
544
+ function ensureWithinSource(srcDir, candidate, importer, importEntry) {
545
+ const rel = relative(srcDir, candidate);
546
+ if (rel.startsWith('..') || rel === '' && candidate !== srcDir) {
547
+ if (rel.startsWith('..')) {
548
+ throw new BundleError(
549
+ BUNDLE_ERROR_CODES.PATH_ESCAPE,
550
+ `Import "${importEntry.specifier}" escapes the src/ directory boundary.`,
551
+ {
552
+ file: importer,
553
+ importer,
554
+ specifier: importEntry.specifier,
555
+ line: importEntry.line,
556
+ },
557
+ );
558
+ }
559
+ }
560
+ }
561
+
562
+ function moduleId(srcDir, filePath) {
563
+ const rel = relative(srcDir, filePath);
564
+ return rel.split(sep).join('/');
565
+ }
566
+
567
+ function validateSyntax(code, filePath) {
568
+ const check = spawnSync(process.execPath, ['--check', '--input-type=module'], {
569
+ input: code,
570
+ encoding: 'utf8',
571
+ maxBuffer: 1024 * 1024,
572
+ });
573
+
574
+ if (check.status === 0) {
575
+ return;
576
+ }
577
+
578
+ const stderr = String(check.stderr || '').trim();
579
+ const lineMatch = stderr.match(/\[stdin\]:(\d+)/);
580
+ const line = lineMatch ? Number(lineMatch[1]) : undefined;
581
+
582
+ const summary = stderr
583
+ .split('\n')
584
+ .map((lineText) => lineText.trim())
585
+ .filter(Boolean)
586
+ .find((lineText) => lineText.startsWith('SyntaxError:'));
587
+
588
+ throw new BundleError(BUNDLE_ERROR_CODES.SYNTAX_ERROR, summary || 'Syntax error in module.', {
589
+ file: filePath,
590
+ line,
591
+ });
592
+ }
593
+
594
+ function hasDynamicImport(code) {
595
+ return /(^|[^\w$.])import\s*\(/m.test(code);
596
+ }
597
+
598
+ function locateToken(code, token) {
599
+ const index = code.indexOf(token);
600
+ if (index < 0) {
601
+ return { line: undefined, column: undefined };
602
+ }
603
+ return lineColumnFromIndex(code, index);
604
+ }
605
+
606
+ function lineColumnFromIndex(code, index) {
607
+ const slice = code.slice(0, index);
608
+ const lines = slice.split(/\r?\n/);
609
+ const line = lines.length;
610
+ const column = lines[lines.length - 1].length + 1;
611
+ return { line, column };
612
+ }
613
+
614
+ function sha1(value) {
615
+ return createHash('sha1').update(value).digest('hex');
616
+ }
617
+
618
+ function relPath(from, to) {
619
+ const rel = relative(from, to);
620
+ return rel.split(sep).join('/');
621
+ }
622
+
623
+ function stopSourceWatch(cache) {
624
+ for (const watcher of cache.watchers) {
625
+ watcher.close();
626
+ }
627
+ cache.watchers = [];
628
+ if (cache.poller) {
629
+ clearInterval(cache.poller);
630
+ cache.poller = null;
631
+ }
632
+ }
633
+
634
+ function startSourceWatch(projectRoot, cache, onChange, onError) {
635
+ stopSourceWatch(cache);
636
+
637
+ const srcDir = resolve(projectRoot, 'src');
638
+ let closed = false;
639
+
640
+ if (!existsSync(srcDir)) {
641
+ throw new BundleError(BUNDLE_ERROR_CODES.MISSING_ENTRY, 'Missing src/ directory for dev watch.', {
642
+ file: srcDir,
643
+ });
644
+ }
645
+
646
+ const close = () => {
647
+ if (closed) return;
648
+ closed = true;
649
+ stopSourceWatch(cache);
650
+ };
651
+
652
+ try {
653
+ const watcher = watch(srcDir, { recursive: true }, (_eventType, filename) => {
654
+ if (!filename) return;
655
+ if (!String(filename).endsWith('.js') && !String(filename).endsWith('.mjs')) return;
656
+ onChange(filename);
657
+ });
658
+ cache.watchers.push(watcher);
659
+ return close;
660
+ } catch (_err) {
661
+ // Fallback for platforms that do not support recursive fs.watch.
662
+ cache.pollFingerprint = fingerprintSourceTree(srcDir);
663
+ cache.poller = setInterval(() => {
664
+ try {
665
+ const next = fingerprintSourceTree(srcDir);
666
+ if (next !== cache.pollFingerprint) {
667
+ cache.pollFingerprint = next;
668
+ onChange('poll');
669
+ }
670
+ } catch (pollError) {
671
+ onError(pollError);
672
+ }
673
+ }, 250);
674
+ return close;
675
+ }
676
+ }
677
+
678
+ function fingerprintSourceTree(rootDir) {
679
+ const files = [];
680
+ walk(rootDir, files);
681
+ files.sort();
682
+ return sha1(files.join('\n'));
683
+ }
684
+
685
+ function walk(dir, output) {
686
+ for (const entry of readdirSync(dir)) {
687
+ const full = join(dir, entry);
688
+ const stat = statSync(full);
689
+ if (stat.isDirectory()) {
690
+ walk(full, output);
691
+ continue;
692
+ }
693
+ if (!(entry.endsWith('.js') || entry.endsWith('.mjs'))) {
694
+ continue;
695
+ }
696
+ output.push(`${full}:${stat.mtimeMs}:${stat.size}`);
697
+ }
698
+ }