Skip to content

Commit 8e8a934

Browse files
committed
chore: adds importmap.json as preferred path
1 parent 92ceb15 commit 8e8a934

9 files changed

Lines changed: 358 additions & 8 deletions

File tree

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ npm install @jspm/node-importmap-loader --save-dev
3232

3333
#### 2. Execute
3434

35-
With a `node.importmap` defined in your working directory, run
35+
With an `importmap.json` defined in your working directory, run
3636

3737
```bash
3838
load-node-importmap <file-to-execute>

src/__tests__/mock-loader.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { resolve as resolveTs } from 'ts-node/esm';
2+
import { URL, pathToFileURL } from 'url';
3+
4+
// Mock modules
5+
const mockLogger = {
6+
info: () => {},
7+
error: () => {},
8+
warn: () => {},
9+
debug: () => {}
10+
};
11+
12+
const mockConfig = {
13+
cacheDir: '/cache/path',
14+
getCachePath: (url) => `/cache/path/${url.split('/').pop()}`
15+
};
16+
17+
const mockParseArgs = {
18+
cacheDir: '/cache/path'
19+
};
20+
21+
const mockModules = new Map([
22+
['./logger.js', mockLogger],
23+
['./config.js', mockConfig],
24+
['./parseArgs.js', mockParseArgs]
25+
]);
26+
27+
export async function resolve(specifier, context, nextResolve) {
28+
if (mockModules.has(specifier)) {
29+
return {
30+
url: pathToFileURL(specifier).href,
31+
shortCircuit: true
32+
};
33+
}
34+
return resolveTs(specifier, context, nextResolve);
35+
}
36+
37+
export async function load(url, context, nextLoad) {
38+
const specifier = new URL(url).pathname;
39+
const mockModule = mockModules.get(specifier);
40+
if (mockModule) {
41+
return {
42+
format: 'module',
43+
shortCircuit: true,
44+
source: `export default ${JSON.stringify(mockModule)};`
45+
};
46+
}
47+
return nextLoad(url, context);
48+
}

src/__tests__/mock-loader.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { resolve as resolveTs } from 'ts-node/esm';
2+
import { URL, pathToFileURL } from 'url';
3+
4+
// Mock modules
5+
const mockLogger = {
6+
info: () => {},
7+
error: () => {},
8+
warn: () => {},
9+
debug: () => {}
10+
};
11+
12+
const mockConfig = {
13+
cacheDir: '/cache/path',
14+
getCachePath: (url: string) => `/cache/path/${url.split('/').pop()}`
15+
};
16+
17+
const mockParseArgs = {
18+
cacheDir: '/cache/path'
19+
};
20+
21+
type MockModule = typeof mockLogger | typeof mockConfig | typeof mockParseArgs;
22+
const mockModules = new Map<string, MockModule>([
23+
['./logger.js', mockLogger],
24+
['./config.js', mockConfig],
25+
['./parseArgs.js', mockParseArgs]
26+
]);
27+
28+
export async function resolve(specifier: string, context: any, nextResolve: any) {
29+
if (mockModules.has(specifier)) {
30+
return {
31+
url: pathToFileURL(specifier).href,
32+
shortCircuit: true
33+
};
34+
}
35+
return resolveTs(specifier, context, nextResolve);
36+
}
37+
38+
export async function load(url: string, context: any, nextLoad: any) {
39+
const specifier = new URL(url).pathname;
40+
const mockModule = mockModules.get(specifier);
41+
if (mockModule) {
42+
return {
43+
format: 'module',
44+
shortCircuit: true,
45+
source: `export default ${JSON.stringify(mockModule)};`
46+
};
47+
}
48+
return nextLoad(url, context);
49+
}

src/__tests__/test-utils.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
2+
3+
// Mock logger
4+
export const mockLogger = {
5+
debug: () => {},
6+
error: () => {},
7+
};
8+
9+
// Store original functions
10+
export const originalFunctions = {
11+
existsSync,
12+
writeFileSync,
13+
mkdirSync,
14+
fetch: global.fetch,
15+
require: require,
16+
};
17+
18+
// Mock Response type
19+
export interface MockResponse extends Response {
20+
ok: boolean;
21+
text: () => Promise<string>;
22+
headers: Headers;
23+
redirected: boolean;
24+
status: number;
25+
statusText: string;
26+
type: ResponseType;
27+
url: string;
28+
clone: () => Response;
29+
body: null;
30+
bodyUsed: boolean;
31+
bytes: () => Promise<Uint8Array>;
32+
arrayBuffer: () => Promise<ArrayBuffer>;
33+
blob: () => Promise<Blob>;
34+
formData: () => Promise<FormData>;
35+
json: () => Promise<any>;
36+
}
37+
38+
// Create a mock Response
39+
export function createMockResponse(options: Partial<MockResponse> = {}): MockResponse {
40+
return {
41+
ok: true,
42+
text: async () => 'const example = true;',
43+
headers: new Headers(),
44+
redirected: false,
45+
status: 200,
46+
statusText: 'OK',
47+
type: 'default' as ResponseType,
48+
url: '',
49+
clone: () => new Response(),
50+
body: null,
51+
bodyUsed: false,
52+
bytes: async () => new Uint8Array(),
53+
arrayBuffer: async () => new ArrayBuffer(0),
54+
blob: async () => new Blob(),
55+
formData: async () => new FormData(),
56+
json: async () => ({}),
57+
...options,
58+
} as MockResponse;
59+
}
60+
61+
// Setup mocks
62+
export function setupMocks(options: {
63+
existsSync?: (path: string) => boolean;
64+
writeFileSync?: (path: string, content: string) => void;
65+
mkdirSync?: (path: string, options?: any) => void;
66+
fetch?: (url: string) => Promise<MockResponse>;
67+
} = {}) {
68+
// Override global functions
69+
Object.defineProperty(global, 'existsSync', {
70+
value: options.existsSync || (() => false),
71+
configurable: true
72+
});
73+
Object.defineProperty(global, 'writeFileSync', {
74+
value: options.writeFileSync || (() => {}),
75+
configurable: true
76+
});
77+
Object.defineProperty(global, 'mkdirSync', {
78+
value: options.mkdirSync || (() => {}),
79+
configurable: true
80+
});
81+
Object.defineProperty(global, 'fetch', {
82+
value: options.fetch || (async () => createMockResponse()),
83+
configurable: true
84+
});
85+
86+
// Override module system
87+
(global as any).require = (module: string) => {
88+
if (module === './logger.js') {
89+
return { logger: () => mockLogger };
90+
}
91+
if (module === './config.js') {
92+
return { isDebuggingEnabled: false };
93+
}
94+
return originalFunctions.require(module);
95+
};
96+
}
97+
98+
// Restore original functions
99+
export function restoreMocks() {
100+
Object.defineProperty(global, 'existsSync', { value: originalFunctions.existsSync });
101+
Object.defineProperty(global, 'writeFileSync', { value: originalFunctions.writeFileSync });
102+
Object.defineProperty(global, 'mkdirSync', { value: originalFunctions.mkdirSync });
103+
Object.defineProperty(global, 'fetch', { value: originalFunctions.fetch });
104+
(global as any).require = originalFunctions.require;
105+
}

src/cache-path.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { describe, it, beforeEach, afterEach } from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { parseNodeModuleCachePath } from './cache-path.js';
4+
import { setupMocks, restoreMocks, createMockResponse, MockResponse, mockLogger } from './__tests__/test-utils.js';
5+
6+
// Mock config
7+
const mockConfig = {
8+
cacheDir: '/cache/path',
9+
getCachePath: (url: string) => `/cache/path/${url.split('/').pop()}`
10+
};
11+
12+
// Mock parseArgs
13+
const mockParseArgs = {
14+
cacheDir: '/cache/path'
15+
};
16+
17+
describe('cache-path', () => {
18+
describe('parseNodeModuleCachePath', () => {
19+
let mockExistsSync: (path: string) => boolean;
20+
let mockWriteFileSync: (path: string, content: string) => void;
21+
let mockMkdirSync: (path: string, options?: any) => void;
22+
let mockFetch: (url: string) => Promise<MockResponse>;
23+
24+
beforeEach(() => {
25+
// Reset mock state
26+
mockExistsSync = () => false;
27+
mockWriteFileSync = () => {};
28+
mockMkdirSync = () => {};
29+
mockFetch = async () => createMockResponse();
30+
31+
// Setup mocks
32+
setupMocks({
33+
existsSync: (path: string) => mockExistsSync(path),
34+
writeFileSync: (path: string, content: string) => mockWriteFileSync(path, content),
35+
mkdirSync: (path: string, options?: any) => mockMkdirSync(path, options),
36+
fetch: (url: string) => mockFetch(url),
37+
});
38+
39+
// Override require to return our mocks
40+
(global as any).require = (module: string) => {
41+
if (module === './logger.js') {
42+
return mockLogger;
43+
}
44+
if (module === './config.js') {
45+
return mockConfig;
46+
}
47+
if (module === './parseArgs.js') {
48+
return mockParseArgs;
49+
}
50+
return (global as any).originalRequire(module);
51+
};
52+
});
53+
54+
afterEach(() => {
55+
restoreMocks();
56+
});
57+
58+
it('returns cache path directly if it exists', async () => {
59+
mockExistsSync = () => true;
60+
const result = await parseNodeModuleCachePath('http://example.com/module.js', '/cache/path/module.js');
61+
assert.equal(result, '/cache/path/module.js');
62+
});
63+
64+
it('downloads and writes to cache if cache path does not exist', async () => {
65+
let writtenPath = '';
66+
let writtenContent = '';
67+
mockWriteFileSync = (path: string, content: string) => {
68+
writtenPath = path;
69+
writtenContent = content;
70+
};
71+
72+
const result = await parseNodeModuleCachePath('http://example.com/module.js', '/cache/path/module.js');
73+
74+
assert.equal(result, '/cache/path/module.js');
75+
assert.equal(writtenPath, '/cache/path/module.js');
76+
assert.equal(writtenContent, 'const example = true;');
77+
});
78+
79+
it('handles failed fetch requests', async () => {
80+
let writeFileCalled = false;
81+
mockWriteFileSync = () => {
82+
writeFileCalled = true;
83+
};
84+
mockFetch = async () => createMockResponse({ ok: false, status: 404, statusText: 'Not Found' });
85+
86+
const result = await parseNodeModuleCachePath('http://example.com/module.js', '/cache/path/module.js');
87+
88+
assert.equal(result, '/cache/path/module.js');
89+
assert.equal(writeFileCalled, false);
90+
});
91+
});
92+
});

src/cache-path.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
2+
import { dirname } from "node:path";
3+
import { logger } from "./logger.js";
4+
import { isDebuggingEnabled as isLogging } from "./config.js";
5+
6+
const log = logger({ file: "cache-path", isLogging });
7+
8+
// Helper function to ensure a file exists
9+
function ensureFileSyncLocal(path: string): void {
10+
try {
11+
const dirPath = dirname(path);
12+
if (!existsSync(dirPath)) {
13+
mkdirSync(dirPath, { recursive: true });
14+
}
15+
writeFileSync(path, "", { flag: "wx" });
16+
} catch (err) {
17+
log.error(`ensureFileSyncLocal: Failed in creating ${path}`);
18+
}
19+
}
20+
21+
/**
22+
* parseNodeModuleCachePath
23+
* @description a convenience function to parse modules
24+
* @param {string} modulePath
25+
* @param {string} cachePath
26+
* @returns {string}
27+
*/
28+
export const parseNodeModuleCachePath = async (modulePath: string, cachePath: string): Promise<string> => {
29+
log.debug("parseNodeModuleCachePath", cachePath, modulePath);
30+
if (existsSync(cachePath)) return cachePath;
31+
try {
32+
const resp = await fetch(modulePath);
33+
if (!resp.ok) throw Error(`404: Module not found: ${modulePath}`);
34+
const code = await resp.text();
35+
ensureFileSyncLocal(cachePath);
36+
writeFileSync(cachePath, code);
37+
return cachePath;
38+
} catch (err) {
39+
log.error(`parseNodeModuleCachePath: Failed in parsing module ${err}`);
40+
return cachePath;
41+
}
42+
};

src/config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ import { argv } from "node:process";
1818
const wd = process.cwd();
1919
export const root = fileURLToPath(`file://${wd}`);
2020
export const cacheMap = new Map();
21-
export const nodeImportMapPath = join(root, "node.importmap");
21+
22+
const importmapJsonPath = join(root, "importmap.json");
23+
const legacyNodeImportmapPath = join(root, "node.importmap");
24+
export const nodeImportMapPath = existsSync(importmapJsonPath)
25+
? importmapJsonPath
26+
: existsSync(legacyNodeImportmapPath)
27+
? legacyNodeImportmapPath
28+
: importmapJsonPath;
29+
2230
export const cache = join(root, ".cache");
2331
const hasCacheFoler = existsSync(cache);
2432
if (!hasCacheFoler) mkdirSync(cache);

src/loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Context, NextResolve } from "./types";
1111
* ******************************************************
1212
* LOADER 🚛
1313
* ------------------------------------------------------
14-
* @summary loads node modules via an *assumed root working directory with a cache and node.importmap*
14+
* @summary loads node modules via an *assumed root working directory with a cache and importmap.json*
1515
* @notes
1616
* The node loader api is being redesigned.
1717
* JSPM will update to the new api when it is stable and

0 commit comments

Comments
 (0)