almostnode 0.2.4 → 0.2.6

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,206 @@
1
+ /**
2
+ * Tailwind Config Loader
3
+ *
4
+ * Parses tailwind.config.ts files and generates JavaScript to configure
5
+ * the Tailwind CDN at runtime via window.tailwind.config.
6
+ */
7
+
8
+ import type { VirtualFS } from '../virtual-fs';
9
+
10
+ export interface TailwindConfigResult {
11
+ /** JavaScript code to set window.tailwind.config (empty string if no config) */
12
+ configScript: string;
13
+ /** Whether config was successfully loaded */
14
+ success: boolean;
15
+ /** Error message if loading failed */
16
+ error?: string;
17
+ }
18
+
19
+ /** Config file names to search for, in priority order */
20
+ const CONFIG_FILE_NAMES = [
21
+ '/tailwind.config.ts',
22
+ '/tailwind.config.js',
23
+ '/tailwind.config.mjs',
24
+ ];
25
+
26
+ /**
27
+ * Load and parse a Tailwind config file from VirtualFS
28
+ */
29
+ export async function loadTailwindConfig(
30
+ vfs: VirtualFS,
31
+ root: string = '/'
32
+ ): Promise<TailwindConfigResult> {
33
+ // Find config file
34
+ let configPath: string | null = null;
35
+ let configContent: string | null = null;
36
+
37
+ for (const fileName of CONFIG_FILE_NAMES) {
38
+ const fullPath = root === '/' ? fileName : `${root}${fileName}`;
39
+ try {
40
+ const content = vfs.readFileSync(fullPath);
41
+ configContent =
42
+ typeof content === 'string'
43
+ ? content
44
+ : content instanceof Uint8Array
45
+ ? new TextDecoder('utf-8').decode(content)
46
+ : Buffer.from(content).toString('utf-8');
47
+ configPath = fullPath;
48
+ break;
49
+ } catch {
50
+ // File not found, try next
51
+ continue;
52
+ }
53
+ }
54
+
55
+ if (!configPath || configContent === null) {
56
+ return {
57
+ configScript: '',
58
+ success: true, // Not an error, just no config
59
+ };
60
+ }
61
+
62
+ try {
63
+ // Strip TypeScript syntax and extract config object
64
+ const jsConfig = stripTypescriptSyntax(configContent);
65
+ const configObject = extractConfigObject(jsConfig);
66
+
67
+ if (!configObject) {
68
+ return {
69
+ configScript: '',
70
+ success: false,
71
+ error: 'Could not extract config object from tailwind.config',
72
+ };
73
+ }
74
+
75
+ // Generate the script to inject
76
+ const configScript = generateConfigScript(configObject);
77
+
78
+ return {
79
+ configScript,
80
+ success: true,
81
+ };
82
+ } catch (error) {
83
+ return {
84
+ configScript: '',
85
+ success: false,
86
+ error: `Failed to parse tailwind.config: ${error instanceof Error ? error.message : String(error)}`,
87
+ };
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Strip TypeScript-specific syntax from config content
93
+ */
94
+ export function stripTypescriptSyntax(content: string): string {
95
+ let result = content;
96
+
97
+ // Remove import type statements
98
+ // e.g., import type { Config } from "tailwindcss"
99
+ result = result.replace(/import\s+type\s+\{[^}]*\}\s+from\s+['"][^'"]*['"]\s*;?\s*/g, '');
100
+
101
+ // Remove regular import statements (Config type, etc.)
102
+ // e.g., import { Config } from "tailwindcss"
103
+ result = result.replace(/import\s+\{[^}]*\}\s+from\s+['"][^'"]*['"]\s*;?\s*/g, '');
104
+
105
+ // Remove satisfies Type assertions
106
+ // e.g., } satisfies Config
107
+ result = result.replace(/\s+satisfies\s+\w+\s*$/gm, '');
108
+ result = result.replace(/\s+satisfies\s+\w+\s*;?\s*$/gm, '');
109
+
110
+ // Remove type annotations on variables
111
+ // e.g., const config: Config = { ... }
112
+ result = result.replace(/:\s*Config\s*=/g, ' =');
113
+
114
+ // Remove 'as const' assertions
115
+ result = result.replace(/\s+as\s+const\s*/g, ' ');
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Extract the config object from the processed content
122
+ */
123
+ export function extractConfigObject(content: string): string | null {
124
+ // Look for export default { ... }
125
+ // We need to find the opening brace and match it to the closing brace
126
+
127
+ // First, find "export default"
128
+ const exportDefaultMatch = content.match(/export\s+default\s*/);
129
+ if (!exportDefaultMatch || exportDefaultMatch.index === undefined) {
130
+ return null;
131
+ }
132
+
133
+ const startIndex = exportDefaultMatch.index + exportDefaultMatch[0].length;
134
+ const remaining = content.substring(startIndex);
135
+
136
+ // Check if it starts with an object literal
137
+ const trimmedRemaining = remaining.trimStart();
138
+ if (!trimmedRemaining.startsWith('{')) {
139
+ return null;
140
+ }
141
+
142
+ // Find the matching closing brace
143
+ const objectStart = startIndex + (remaining.length - trimmedRemaining.length);
144
+ const objectContent = content.substring(objectStart);
145
+
146
+ let braceCount = 0;
147
+ let inString = false;
148
+ let stringChar = '';
149
+ let escaped = false;
150
+ let endIndex = -1;
151
+
152
+ for (let i = 0; i < objectContent.length; i++) {
153
+ const char = objectContent[i];
154
+
155
+ if (escaped) {
156
+ escaped = false;
157
+ continue;
158
+ }
159
+
160
+ if (char === '\\') {
161
+ escaped = true;
162
+ continue;
163
+ }
164
+
165
+ if (inString) {
166
+ if (char === stringChar) {
167
+ inString = false;
168
+ }
169
+ continue;
170
+ }
171
+
172
+ if (char === '"' || char === "'" || char === '`') {
173
+ inString = true;
174
+ stringChar = char;
175
+ continue;
176
+ }
177
+
178
+ if (char === '{') {
179
+ braceCount++;
180
+ } else if (char === '}') {
181
+ braceCount--;
182
+ if (braceCount === 0) {
183
+ endIndex = i + 1;
184
+ break;
185
+ }
186
+ }
187
+ }
188
+
189
+ if (endIndex === -1) {
190
+ return null;
191
+ }
192
+
193
+ return objectContent.substring(0, endIndex);
194
+ }
195
+
196
+ /**
197
+ * Generate the script to inject the Tailwind config
198
+ */
199
+ export function generateConfigScript(configObject: string): string {
200
+ // Wrap in a script that sets tailwind.config
201
+ // This must run AFTER the Tailwind CDN script loads
202
+ // The CDN creates the global `tailwind` object, then we configure it
203
+ return `<script>
204
+ tailwind.config = ${configObject};
205
+ </script>`;
206
+ }
@@ -6,6 +6,7 @@
6
6
  import { DevServer, DevServerOptions, ResponseData, HMRUpdate } from '../dev-server';
7
7
  import { VirtualFS } from '../virtual-fs';
8
8
  import { Buffer } from '../shims/stream';
9
+ import { simpleHash } from '../utils/hash';
9
10
 
10
11
  // Check if we're in a real browser environment (not jsdom or Node.js)
11
12
  // jsdom has window but doesn't have ServiceWorker or SharedArrayBuffer
@@ -279,6 +280,7 @@ export class ViteDevServer extends DevServer {
279
280
  private watcherCleanup: (() => void) | null = null;
280
281
  private options: ViteDevServerOptions;
281
282
  private hmrTargetWindow: Window | null = null;
283
+ private transformCache: Map<string, { code: string; hash: string }> = new Map();
282
284
 
283
285
  constructor(vfs: VirtualFS, options: ViteDevServerOptions) {
284
286
  super(vfs, options);
@@ -478,8 +480,31 @@ export class ViteDevServer extends DevServer {
478
480
  private async transformAndServe(filePath: string, urlPath: string): Promise<ResponseData> {
479
481
  try {
480
482
  const content = this.vfs.readFileSync(filePath, 'utf8');
483
+ const hash = simpleHash(content);
484
+
485
+ // Check transform cache
486
+ const cached = this.transformCache.get(filePath);
487
+ if (cached && cached.hash === hash) {
488
+ const buffer = Buffer.from(cached.code);
489
+ return {
490
+ statusCode: 200,
491
+ statusMessage: 'OK',
492
+ headers: {
493
+ 'Content-Type': 'application/javascript; charset=utf-8',
494
+ 'Content-Length': String(buffer.length),
495
+ 'Cache-Control': 'no-cache',
496
+ 'X-Transformed': 'true',
497
+ 'X-Cache': 'hit',
498
+ },
499
+ body: buffer,
500
+ };
501
+ }
502
+
481
503
  const transformed = await this.transformCode(content, urlPath);
482
504
 
505
+ // Cache the transform result
506
+ this.transformCache.set(filePath, { code: transformed, hash });
507
+
483
508
  const buffer = Buffer.from(transformed);
484
509
  return {
485
510
  statusCode: 200,
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Macaly Demo - Load the REAL macaly-web repository into almostnode
3
+ * This tests almostnode's ability to run a real-world Next.js app
4
+ */
5
+
6
+ import { VirtualFS } from './virtual-fs';
7
+ import { createRuntime } from './create-runtime';
8
+ import type { IRuntime } from './runtime-interface';
9
+ import { NextDevServer } from './frameworks/next-dev-server';
10
+ import { getServerBridge } from './server-bridge';
11
+
12
+ /**
13
+ * Files to load from the real macaly-web repository
14
+ * We'll populate this dynamically, but here's the structure we need
15
+ */
16
+ export interface MacalyFiles {
17
+ [path: string]: string;
18
+ }
19
+
20
+ /**
21
+ * Load the real macaly-web project into VirtualFS
22
+ */
23
+ export function loadMacalyProject(vfs: VirtualFS, files: MacalyFiles): void {
24
+ for (const [path, content] of Object.entries(files)) {
25
+ // Ensure directory exists
26
+ const dir = path.substring(0, path.lastIndexOf('/'));
27
+ if (dir) {
28
+ vfs.mkdirSync(dir, { recursive: true });
29
+ }
30
+
31
+ // Handle base64-encoded binary files
32
+ if (content.startsWith('base64:')) {
33
+ const base64Data = content.slice(7); // Remove 'base64:' prefix
34
+ const binaryData = atob(base64Data);
35
+ const bytes = new Uint8Array(binaryData.length);
36
+ for (let i = 0; i < binaryData.length; i++) {
37
+ bytes[i] = binaryData.charCodeAt(i);
38
+ }
39
+ vfs.writeFileSync(path, Buffer.from(bytes));
40
+ } else {
41
+ vfs.writeFileSync(path, content);
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Initialize the Macaly demo with real files
48
+ */
49
+ export async function initMacalyDemo(
50
+ outputElement: HTMLElement,
51
+ files: MacalyFiles,
52
+ options: { useWorker?: boolean } = {}
53
+ ): Promise<{ vfs: VirtualFS; runtime: IRuntime }> {
54
+ const { useWorker = false } = options;
55
+
56
+ const log = (message: string) => {
57
+ const line = document.createElement('div');
58
+ line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
59
+ outputElement.appendChild(line);
60
+ outputElement.scrollTop = outputElement.scrollHeight;
61
+ };
62
+
63
+ log('Creating virtual file system...');
64
+ const vfs = new VirtualFS();
65
+
66
+ log(`Loading ${Object.keys(files).length} files from macaly-web...`);
67
+ loadMacalyProject(vfs, files);
68
+
69
+ log(`Initializing runtime (${useWorker ? 'Web Worker mode' : 'main thread'})...`);
70
+ const runtime = await createRuntime(vfs, {
71
+ dangerouslyAllowSameOrigin: true,
72
+ useWorker,
73
+ cwd: '/',
74
+ env: {
75
+ NODE_ENV: 'development',
76
+ },
77
+ onConsole: (method, args) => {
78
+ const prefix = method === 'error' ? '[ERROR]' : method === 'warn' ? '[WARN]' : '';
79
+ log(`${prefix} ${args.map((a) => String(a)).join(' ')}`);
80
+ },
81
+ });
82
+
83
+ if (useWorker) {
84
+ log('Runtime is running in a Web Worker for better UI responsiveness');
85
+ }
86
+
87
+ log('Setting up file watcher...');
88
+ vfs.watch('/app', { recursive: true }, (eventType, filename) => {
89
+ log(`File ${eventType}: ${filename}`);
90
+ });
91
+
92
+ log('Macaly demo initialized!');
93
+
94
+ return { vfs, runtime };
95
+ }
96
+
97
+ /**
98
+ * Start the Macaly dev server
99
+ */
100
+ export async function startMacalyDevServer(
101
+ vfs: VirtualFS,
102
+ options: {
103
+ port?: number;
104
+ log?: (message: string) => void;
105
+ } = {}
106
+ ): Promise<{
107
+ server: NextDevServer;
108
+ url: string;
109
+ stop: () => void;
110
+ }> {
111
+ const port = options.port || 3001;
112
+ const log = options.log || console.log;
113
+
114
+ log('Starting Macaly dev server...');
115
+
116
+ const server = new NextDevServer(vfs, { port, root: '/' });
117
+ const bridge = getServerBridge();
118
+
119
+ try {
120
+ log('Initializing Service Worker...');
121
+ await bridge.initServiceWorker();
122
+ log('Service Worker ready');
123
+ } catch (error) {
124
+ log(`Warning: Service Worker failed to initialize: ${error}`);
125
+ }
126
+
127
+ bridge.on('server-ready', (p: unknown, u: unknown) => {
128
+ log(`Server ready at ${u}`);
129
+ });
130
+
131
+ const httpServer = {
132
+ listening: true,
133
+ address: () => ({ port: server.getPort(), address: '0.0.0.0', family: 'IPv4' }),
134
+ async handleRequest(
135
+ method: string,
136
+ url: string,
137
+ headers: Record<string, string>,
138
+ body?: string | ArrayBuffer
139
+ ) {
140
+ const bodyBuffer = body
141
+ ? typeof body === 'string'
142
+ ? Buffer.from(body)
143
+ : Buffer.from(body)
144
+ : undefined;
145
+ return server.handleRequest(method, url, headers, bodyBuffer);
146
+ },
147
+ };
148
+
149
+ bridge.registerServer(httpServer as any, port);
150
+ server.start();
151
+ log('File watcher started');
152
+
153
+ server.on('hmr-update', (update: unknown) => {
154
+ log(`HMR update: ${JSON.stringify(update)}`);
155
+ });
156
+
157
+ const url = bridge.getServerUrl(port);
158
+ log(`Macaly dev server running at: ${url}/`);
159
+
160
+ return {
161
+ server,
162
+ url: url + '/',
163
+ stop: () => {
164
+ server.stop();
165
+ bridge.unregisterServer(port);
166
+ },
167
+ };
168
+ }
169
+
170
+ // Export for use in the demo page
171
+ export { VirtualFS, NextDevServer, createRuntime };
172
+ export type { IRuntime };
package/src/runtime.ts CHANGED
@@ -7,6 +7,8 @@
7
7
 
8
8
  import { VirtualFS } from './virtual-fs';
9
9
  import type { IRuntime, IExecuteResult, IRuntimeOptions } from './runtime-interface';
10
+ import type { PackageJson } from './types/package-json';
11
+ import { simpleHash } from './utils/hash';
10
12
  import { createFsShim, FsShim } from './shims/fs';
11
13
  import * as pathShim from './shims/path';
12
14
  import { createProcess, Process } from './shims/process';
@@ -347,8 +349,30 @@ function createRequire(
347
349
  process: Process,
348
350
  currentDir: string,
349
351
  moduleCache: Record<string, Module>,
350
- options: RuntimeOptions
352
+ options: RuntimeOptions,
353
+ processedCodeCache?: Map<string, string>
351
354
  ): RequireFunction {
355
+ // Module resolution cache for faster repeated imports
356
+ const resolutionCache: Map<string, string | null> = new Map();
357
+
358
+ // Package.json parsing cache
359
+ const packageJsonCache: Map<string, PackageJson | null> = new Map();
360
+
361
+ const getParsedPackageJson = (pkgPath: string): PackageJson | null => {
362
+ if (packageJsonCache.has(pkgPath)) {
363
+ return packageJsonCache.get(pkgPath)!;
364
+ }
365
+ try {
366
+ const content = vfs.readFileSync(pkgPath, 'utf8');
367
+ const parsed = JSON.parse(content) as PackageJson;
368
+ packageJsonCache.set(pkgPath, parsed);
369
+ return parsed;
370
+ } catch {
371
+ packageJsonCache.set(pkgPath, null);
372
+ return null;
373
+ }
374
+ };
375
+
352
376
  const resolveModule = (id: string, fromDir: string): string => {
353
377
  // Handle node: protocol prefix (Node.js 16+)
354
378
  if (id.startsWith('node:')) {
@@ -360,6 +384,16 @@ function createRequire(
360
384
  return id;
361
385
  }
362
386
 
387
+ // Check resolution cache
388
+ const cacheKey = `${fromDir}|${id}`;
389
+ const cached = resolutionCache.get(cacheKey);
390
+ if (cached !== undefined) {
391
+ if (cached === null) {
392
+ throw new Error(`Cannot find module '${id}'`);
393
+ }
394
+ return cached;
395
+ }
396
+
363
397
  // Relative paths
364
398
  if (id.startsWith('./') || id.startsWith('../') || id.startsWith('/')) {
365
399
  const resolved = id.startsWith('/')
@@ -370,11 +404,13 @@ function createRequire(
370
404
  if (vfs.existsSync(resolved)) {
371
405
  const stats = vfs.statSync(resolved);
372
406
  if (stats.isFile()) {
407
+ resolutionCache.set(cacheKey, resolved);
373
408
  return resolved;
374
409
  }
375
410
  // Directory - look for index.js
376
411
  const indexPath = pathShim.join(resolved, 'index.js');
377
412
  if (vfs.existsSync(indexPath)) {
413
+ resolutionCache.set(cacheKey, indexPath);
378
414
  return indexPath;
379
415
  }
380
416
  }
@@ -384,10 +420,12 @@ function createRequire(
384
420
  for (const ext of extensions) {
385
421
  const withExt = resolved + ext;
386
422
  if (vfs.existsSync(withExt)) {
423
+ resolutionCache.set(cacheKey, withExt);
387
424
  return withExt;
388
425
  }
389
426
  }
390
427
 
428
+ resolutionCache.set(cacheKey, null);
391
429
  throw new Error(`Cannot find module '${id}' from '${fromDir}'`);
392
430
  }
393
431
 
@@ -436,10 +474,8 @@ function createRequire(
436
474
  const pkgRoot = pathShim.join(nodeModulesDir, pkgName);
437
475
  const pkgPath = pathShim.join(pkgRoot, 'package.json');
438
476
 
439
- if (vfs.existsSync(pkgPath)) {
440
- const pkgContent = vfs.readFileSync(pkgPath, 'utf8');
441
- const pkg = JSON.parse(pkgContent);
442
-
477
+ const pkg = getParsedPackageJson(pkgPath);
478
+ if (pkg) {
443
479
  // Use resolve.exports to handle the exports field
444
480
  if (pkg.exports) {
445
481
  try {
@@ -474,15 +510,22 @@ function createRequire(
474
510
  while (searchDir !== '/') {
475
511
  const nodeModulesDir = pathShim.join(searchDir, 'node_modules');
476
512
  const resolved = tryResolveFromNodeModules(nodeModulesDir, id);
477
- if (resolved) return resolved;
513
+ if (resolved) {
514
+ resolutionCache.set(cacheKey, resolved);
515
+ return resolved;
516
+ }
478
517
 
479
518
  searchDir = pathShim.dirname(searchDir);
480
519
  }
481
520
 
482
521
  // Try root node_modules as last resort
483
522
  const rootResolved = tryResolveFromNodeModules('/node_modules', id);
484
- if (rootResolved) return rootResolved;
523
+ if (rootResolved) {
524
+ resolutionCache.set(cacheKey, rootResolved);
525
+ return rootResolved;
526
+ }
485
527
 
528
+ resolutionCache.set(cacheKey, null);
486
529
  throw new Error(`Cannot find module '${id}'`);
487
530
  };
488
531
 
@@ -514,28 +557,40 @@ function createRequire(
514
557
  }
515
558
 
516
559
  // Read and execute JS file
517
- let code = vfs.readFileSync(resolvedPath, 'utf8');
560
+ const rawCode = vfs.readFileSync(resolvedPath, 'utf8');
518
561
  const dirname = pathShim.dirname(resolvedPath);
519
562
 
520
- // Transform ESM to CJS if needed (for .mjs files or ESM that wasn't pre-transformed)
521
- // This handles files that weren't transformed during npm install
522
- // BUT skip .cjs files and already-bundled CJS code
523
- const isCjsFile = resolvedPath.endsWith('.cjs');
524
- const isAlreadyBundledCjs = code.startsWith('"use strict";\nvar __') ||
525
- code.startsWith("'use strict';\nvar __");
563
+ // Check processed code cache (useful for HMR when module cache is cleared but code hasn't changed)
564
+ // Use a simple hash of the content for cache key to handle content changes
565
+ const codeCacheKey = `${resolvedPath}|${simpleHash(rawCode)}`;
566
+ let code = processedCodeCache?.get(codeCacheKey);
567
+
568
+ if (!code) {
569
+ code = rawCode;
526
570
 
527
- const hasEsmImport = /\bimport\s+[\w{*'"]/m.test(code);
528
- const hasEsmExport = /\bexport\s+(?:default|const|let|var|function|class|{|\*)/m.test(code);
571
+ // Transform ESM to CJS if needed (for .mjs files or ESM that wasn't pre-transformed)
572
+ // This handles files that weren't transformed during npm install
573
+ // BUT skip .cjs files and already-bundled CJS code
574
+ const isCjsFile = resolvedPath.endsWith('.cjs');
575
+ const isAlreadyBundledCjs = code.startsWith('"use strict";\nvar __') ||
576
+ code.startsWith("'use strict';\nvar __");
529
577
 
530
- if (!isCjsFile && !isAlreadyBundledCjs) {
531
- if (resolvedPath.endsWith('.mjs') || resolvedPath.includes('/esm/') || hasEsmImport || hasEsmExport) {
532
- code = transformEsmToCjs(code, resolvedPath);
578
+ const hasEsmImport = /\bimport\s+[\w{*'"]/m.test(code);
579
+ const hasEsmExport = /\bexport\s+(?:default|const|let|var|function|class|{|\*)/m.test(code);
580
+
581
+ if (!isCjsFile && !isAlreadyBundledCjs) {
582
+ if (resolvedPath.endsWith('.mjs') || resolvedPath.includes('/esm/') || hasEsmImport || hasEsmExport) {
583
+ code = transformEsmToCjs(code, resolvedPath);
584
+ }
533
585
  }
534
- }
535
586
 
536
- // Transform dynamic imports: import('x') -> __dynamicImport('x')
537
- // This allows dynamic imports to work in our eval-based runtime
538
- code = transformDynamicImports(code);
587
+ // Transform dynamic imports: import('x') -> __dynamicImport('x')
588
+ // This allows dynamic imports to work in our eval-based runtime
589
+ code = transformDynamicImports(code);
590
+
591
+ // Cache the processed code
592
+ processedCodeCache?.set(codeCacheKey, code);
593
+ }
539
594
 
540
595
  // Create require for this module
541
596
  const moduleRequire = createRequire(
@@ -544,7 +599,8 @@ function createRequire(
544
599
  process,
545
600
  dirname,
546
601
  moduleCache,
547
- options
602
+ options,
603
+ processedCodeCache
548
604
  );
549
605
  moduleRequire.cache = moduleCache;
550
606
 
@@ -779,6 +835,8 @@ export class Runtime {
779
835
  private process: Process;
780
836
  private moduleCache: Record<string, Module> = {};
781
837
  private options: RuntimeOptions;
838
+ /** Cache for pre-processed code (after ESM transform) before eval */
839
+ private processedCodeCache: Map<string, string> = new Map();
782
840
 
783
841
  constructor(vfs: VirtualFS, options: RuntimeOptions = {}) {
784
842
  this.vfs = vfs;
@@ -907,7 +965,8 @@ export class Runtime {
907
965
  this.process,
908
966
  dirname,
909
967
  this.moduleCache,
910
- this.options
968
+ this.options,
969
+ this.processedCodeCache
911
970
  );
912
971
 
913
972
  // Create module object
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Type definition for package.json files
3
+ */
4
+ export interface PackageJson {
5
+ name?: string;
6
+ version?: string;
7
+ main?: string;
8
+ module?: string;
9
+ types?: string;
10
+ exports?: Record<string, unknown> | string;
11
+ dependencies?: Record<string, string>;
12
+ devDependencies?: Record<string, string>;
13
+ peerDependencies?: Record<string, string>;
14
+ [key: string]: unknown;
15
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Simple hash function for content-based cache invalidation.
3
+ * Uses djb2-style hashing for fast string hashing.
4
+ */
5
+ export function simpleHash(str: string): string {
6
+ let hash = 0;
7
+ for (let i = 0; i < str.length; i++) {
8
+ hash = ((hash << 5) - hash) + str.charCodeAt(i);
9
+ hash |= 0;
10
+ }
11
+ return hash.toString(36);
12
+ }