@esmx/import 3.0.0-rc.30 → 3.0.0-rc.31
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.
- package/dist/error.d.ts +16 -0
- package/dist/error.mjs +88 -0
- package/dist/error.test.d.ts +1 -0
- package/dist/error.test.mjs +161 -0
- package/dist/import-vm.mjs +15 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +7 -0
- package/package.json +3 -3
- package/src/error.test.ts +196 -0
- package/src/error.ts +150 -0
- package/src/import-vm.ts +15 -7
- package/src/index.ts +7 -0
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const formatCircularDependency: (moduleIds: string[], targetModule: string) => string;
|
|
2
|
+
export declare const formatModuleChain: (moduleIds: string[], targetModule: string, originalError?: Error) => string;
|
|
3
|
+
export declare class ModuleLoadingError extends Error {
|
|
4
|
+
moduleIds: string[];
|
|
5
|
+
targetModule: string;
|
|
6
|
+
originalError?: Error | undefined;
|
|
7
|
+
constructor(message: string, moduleIds: string[], targetModule: string, originalError?: Error | undefined);
|
|
8
|
+
}
|
|
9
|
+
export declare class CircularDependencyError extends ModuleLoadingError {
|
|
10
|
+
constructor(message: string, moduleIds: string[], targetModule: string);
|
|
11
|
+
toString(): string;
|
|
12
|
+
}
|
|
13
|
+
export declare class FileReadError extends ModuleLoadingError {
|
|
14
|
+
constructor(message: string, moduleIds: string[], targetModule: string, originalError?: Error);
|
|
15
|
+
toString(): string;
|
|
16
|
+
}
|
package/dist/error.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const Colors = {
|
|
3
|
+
RED: "\x1B[31m",
|
|
4
|
+
YELLOW: "\x1B[33m",
|
|
5
|
+
CYAN: "\x1B[36m",
|
|
6
|
+
GRAY: "\x1B[90m",
|
|
7
|
+
RESET: "\x1B[0m",
|
|
8
|
+
BOLD: "\x1B[1m"
|
|
9
|
+
};
|
|
10
|
+
const supportsColor = () => {
|
|
11
|
+
return !!(process.stdout?.isTTY && process.env.TERM !== "dumb") || process.env.FORCE_COLOR === "1" || process.env.FORCE_COLOR === "true";
|
|
12
|
+
};
|
|
13
|
+
const useColors = supportsColor() && process.env.NO_COLOR !== "1";
|
|
14
|
+
const colorize = (text, color) => {
|
|
15
|
+
return useColors ? `${color}${text}${Colors.RESET}` : text;
|
|
16
|
+
};
|
|
17
|
+
const getRelativeFromCwd = (filePath) => {
|
|
18
|
+
return path.relative(process.cwd(), filePath);
|
|
19
|
+
};
|
|
20
|
+
export const formatCircularDependency = (moduleIds, targetModule) => {
|
|
21
|
+
const fullChain = [...moduleIds, targetModule];
|
|
22
|
+
return `${colorize(colorize("Circular dependency:", Colors.BOLD), Colors.RED)}
|
|
23
|
+
${fullChain.map((module, index) => {
|
|
24
|
+
const isLastModule = index === fullChain.length - 1;
|
|
25
|
+
const prefix = index === 0 ? "\u250C\u2500 " : index === fullChain.length - 1 ? "\u2514\u2500 " : "\u251C\u2500 ";
|
|
26
|
+
const displayPath = getRelativeFromCwd(module);
|
|
27
|
+
const isCircularModule = fullChain.filter((m) => m === module).length > 1;
|
|
28
|
+
const coloredFile = isCircularModule ? colorize(colorize(displayPath, Colors.BOLD), Colors.RED) : colorize(displayPath, Colors.CYAN);
|
|
29
|
+
const suffix = isLastModule ? ` ${colorize("\u2190 circular", Colors.YELLOW)}` : "";
|
|
30
|
+
return `${colorize(prefix, Colors.GRAY)}${coloredFile}${suffix}`;
|
|
31
|
+
}).join("\n")}`;
|
|
32
|
+
};
|
|
33
|
+
export const formatModuleChain = (moduleIds, targetModule, originalError) => {
|
|
34
|
+
let result = "";
|
|
35
|
+
if (moduleIds.length === 0) {
|
|
36
|
+
const displayPath = getRelativeFromCwd(targetModule);
|
|
37
|
+
result = `${colorize("Failed to load:", Colors.CYAN)} ${colorize(displayPath, Colors.RED)}`;
|
|
38
|
+
} else {
|
|
39
|
+
const chain = [...moduleIds, targetModule];
|
|
40
|
+
result = `${colorize(colorize("Import chain:", Colors.BOLD), Colors.CYAN)}
|
|
41
|
+
${chain.map((module, index) => {
|
|
42
|
+
const indent = " ".repeat(index);
|
|
43
|
+
const connector = index === 0 ? "" : "\u2514\u2500 ";
|
|
44
|
+
const displayPath = getRelativeFromCwd(module);
|
|
45
|
+
const isFailedFile = index === chain.length - 1;
|
|
46
|
+
const coloredFile = isFailedFile ? colorize(colorize(displayPath, Colors.BOLD), Colors.RED) : colorize(displayPath, Colors.CYAN);
|
|
47
|
+
const status = isFailedFile ? ` ${colorize(colorize("\u2717 FAILED", Colors.BOLD), Colors.RED)}` : "";
|
|
48
|
+
return `${colorize(indent + connector, Colors.GRAY)}${coloredFile}${status}`;
|
|
49
|
+
}).join("\n")}`;
|
|
50
|
+
}
|
|
51
|
+
if (originalError) {
|
|
52
|
+
result += `
|
|
53
|
+
|
|
54
|
+
${colorize("Cause:", Colors.YELLOW)} ${originalError.message}`;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
};
|
|
58
|
+
export class ModuleLoadingError extends Error {
|
|
59
|
+
constructor(message, moduleIds, targetModule, originalError) {
|
|
60
|
+
super(message);
|
|
61
|
+
this.moduleIds = moduleIds;
|
|
62
|
+
this.targetModule = targetModule;
|
|
63
|
+
this.originalError = originalError;
|
|
64
|
+
this.name = "ModuleLoadingError";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export class CircularDependencyError extends ModuleLoadingError {
|
|
68
|
+
constructor(message, moduleIds, targetModule) {
|
|
69
|
+
super(message, moduleIds, targetModule);
|
|
70
|
+
this.name = "CircularDependencyError";
|
|
71
|
+
}
|
|
72
|
+
toString() {
|
|
73
|
+
return `${this.name}: ${this.message}
|
|
74
|
+
|
|
75
|
+
${formatCircularDependency(this.moduleIds, this.targetModule)}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export class FileReadError extends ModuleLoadingError {
|
|
79
|
+
constructor(message, moduleIds, targetModule, originalError) {
|
|
80
|
+
super(message, moduleIds, targetModule, originalError);
|
|
81
|
+
this.name = "FileReadError";
|
|
82
|
+
}
|
|
83
|
+
toString() {
|
|
84
|
+
return `${this.name}: ${this.message}
|
|
85
|
+
|
|
86
|
+
${formatModuleChain(this.moduleIds, this.targetModule, this.originalError)}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
CircularDependencyError,
|
|
5
|
+
FileReadError,
|
|
6
|
+
ModuleLoadingError,
|
|
7
|
+
formatCircularDependency,
|
|
8
|
+
formatModuleChain
|
|
9
|
+
} from "./error.mjs";
|
|
10
|
+
describe("Module Loading Errors", () => {
|
|
11
|
+
let originalEnv;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
originalEnv = process.env.NO_COLOR;
|
|
14
|
+
process.env.NO_COLOR = "1";
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
if (originalEnv !== void 0) {
|
|
18
|
+
process.env.NO_COLOR = originalEnv;
|
|
19
|
+
} else {
|
|
20
|
+
process.env.NO_COLOR = void 0;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
describe("CircularDependencyError", () => {
|
|
24
|
+
it("should create circular dependency error with correct properties", () => {
|
|
25
|
+
const moduleIds = ["/src/A.js", "/src/B.js"];
|
|
26
|
+
const targetModule = "/src/A.js";
|
|
27
|
+
const error = new CircularDependencyError(
|
|
28
|
+
"Test circular dependency",
|
|
29
|
+
moduleIds,
|
|
30
|
+
targetModule
|
|
31
|
+
);
|
|
32
|
+
expect(error.name).toBe("CircularDependencyError");
|
|
33
|
+
expect(error.message).toBe("Test circular dependency");
|
|
34
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
35
|
+
expect(error.targetModule).toBe(targetModule);
|
|
36
|
+
expect(error instanceof ModuleLoadingError).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it("should format circular dependency in toString", () => {
|
|
39
|
+
const moduleIds = ["/src/A.js", "/src/B.js"];
|
|
40
|
+
const targetModule = "/src/A.js";
|
|
41
|
+
const error = new CircularDependencyError(
|
|
42
|
+
"Circular dependency detected",
|
|
43
|
+
moduleIds,
|
|
44
|
+
targetModule
|
|
45
|
+
);
|
|
46
|
+
const formatted = error.toString();
|
|
47
|
+
const relativeA = path.relative(process.cwd(), "/src/A.js");
|
|
48
|
+
const relativeB = path.relative(process.cwd(), "/src/B.js");
|
|
49
|
+
expect(formatted).toContain("CircularDependencyError");
|
|
50
|
+
expect(formatted).toContain("Circular dependency detected");
|
|
51
|
+
expect(formatted).toContain(`\u250C\u2500 ${relativeA}`);
|
|
52
|
+
expect(formatted).toContain(`\u251C\u2500 ${relativeB}`);
|
|
53
|
+
expect(formatted).toContain(`\u2514\u2500 ${relativeA}`);
|
|
54
|
+
expect(formatted).toContain("circular");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("FileReadError", () => {
|
|
58
|
+
it("should create file read error with correct properties", () => {
|
|
59
|
+
const moduleIds = ["/src/main.js", "/src/components/App.js"];
|
|
60
|
+
const targetModule = "/src/missing.js";
|
|
61
|
+
const originalError = new Error(
|
|
62
|
+
"ENOENT: no such file or directory"
|
|
63
|
+
);
|
|
64
|
+
const error = new FileReadError(
|
|
65
|
+
"Failed to read module",
|
|
66
|
+
moduleIds,
|
|
67
|
+
targetModule,
|
|
68
|
+
originalError
|
|
69
|
+
);
|
|
70
|
+
expect(error.name).toBe("FileReadError");
|
|
71
|
+
expect(error.message).toBe("Failed to read module");
|
|
72
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
73
|
+
expect(error.targetModule).toBe(targetModule);
|
|
74
|
+
expect(error.originalError).toBe(originalError);
|
|
75
|
+
expect(error instanceof ModuleLoadingError).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it("should format module chain in toString", () => {
|
|
78
|
+
const moduleIds = ["/src/main.js", "/src/components/App.js"];
|
|
79
|
+
const targetModule = "/src/missing.js";
|
|
80
|
+
const originalError = new Error(
|
|
81
|
+
"ENOENT: no such file or directory"
|
|
82
|
+
);
|
|
83
|
+
const error = new FileReadError(
|
|
84
|
+
"Failed to read module",
|
|
85
|
+
moduleIds,
|
|
86
|
+
targetModule,
|
|
87
|
+
originalError
|
|
88
|
+
);
|
|
89
|
+
const formatted = error.toString();
|
|
90
|
+
expect(formatted).toContain("FileReadError");
|
|
91
|
+
expect(formatted).toContain("Failed to read module");
|
|
92
|
+
expect(formatted).toContain("Import chain:");
|
|
93
|
+
expect(formatted).toContain("main.js");
|
|
94
|
+
expect(formatted).toContain("App.js");
|
|
95
|
+
expect(formatted).toContain("missing.js");
|
|
96
|
+
expect(formatted).toContain("\u2717 FAILED");
|
|
97
|
+
expect(formatted).toContain("Cause:");
|
|
98
|
+
expect(formatted).toContain("ENOENT");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("Formatting Functions", () => {
|
|
102
|
+
it("should format circular dependency correctly", () => {
|
|
103
|
+
const moduleIds = ["/src/A.js", "/src/B.js", "/src/C.js"];
|
|
104
|
+
const targetModule = "/src/A.js";
|
|
105
|
+
const formatted = formatCircularDependency(moduleIds, targetModule);
|
|
106
|
+
const relativeA = path.relative(process.cwd(), "/src/A.js");
|
|
107
|
+
const relativeB = path.relative(process.cwd(), "/src/B.js");
|
|
108
|
+
const relativeC = path.relative(process.cwd(), "/src/C.js");
|
|
109
|
+
expect(formatted).toContain("Circular dependency:");
|
|
110
|
+
expect(formatted).toContain(`\u250C\u2500 ${relativeA}`);
|
|
111
|
+
expect(formatted).toContain(`\u251C\u2500 ${relativeB}`);
|
|
112
|
+
expect(formatted).toContain(`\u251C\u2500 ${relativeC}`);
|
|
113
|
+
expect(formatted).toContain(`\u2514\u2500 ${relativeA}`);
|
|
114
|
+
expect(formatted).toContain("circular");
|
|
115
|
+
});
|
|
116
|
+
it("should format module chain correctly", () => {
|
|
117
|
+
const moduleIds = ["/src/main.js", "/src/app.js"];
|
|
118
|
+
const targetModule = "/src/missing.js";
|
|
119
|
+
const originalError = new Error("File not found");
|
|
120
|
+
const formatted = formatModuleChain(
|
|
121
|
+
moduleIds,
|
|
122
|
+
targetModule,
|
|
123
|
+
originalError
|
|
124
|
+
);
|
|
125
|
+
expect(formatted).toContain("Import chain:");
|
|
126
|
+
expect(formatted).toContain("main.js");
|
|
127
|
+
expect(formatted).toContain("app.js");
|
|
128
|
+
expect(formatted).toContain("missing.js");
|
|
129
|
+
expect(formatted).toContain("\u2717 FAILED");
|
|
130
|
+
expect(formatted).toContain("Cause:");
|
|
131
|
+
expect(formatted).toContain("File not found");
|
|
132
|
+
});
|
|
133
|
+
it("should handle empty moduleIds in formatModuleChain", () => {
|
|
134
|
+
const moduleIds = [];
|
|
135
|
+
const targetModule = "/src/standalone.js";
|
|
136
|
+
const formatted = formatModuleChain(moduleIds, targetModule);
|
|
137
|
+
expect(formatted).toContain("Failed to load:");
|
|
138
|
+
expect(formatted).toContain("standalone.js");
|
|
139
|
+
});
|
|
140
|
+
it("should handle deep module chains", () => {
|
|
141
|
+
const moduleIds = [
|
|
142
|
+
"/src/a.js",
|
|
143
|
+
"/src/b.js",
|
|
144
|
+
"/src/c.js",
|
|
145
|
+
"/src/d.js"
|
|
146
|
+
];
|
|
147
|
+
const targetModule = "/src/e.js";
|
|
148
|
+
const formatted = formatModuleChain(moduleIds, targetModule);
|
|
149
|
+
const relativeA = path.relative(process.cwd(), "/src/a.js");
|
|
150
|
+
const relativeB = path.relative(process.cwd(), "/src/b.js");
|
|
151
|
+
const relativeC = path.relative(process.cwd(), "/src/c.js");
|
|
152
|
+
const relativeD = path.relative(process.cwd(), "/src/d.js");
|
|
153
|
+
const relativeE = path.relative(process.cwd(), "/src/e.js");
|
|
154
|
+
expect(formatted).toContain(relativeA);
|
|
155
|
+
expect(formatted).toContain(` \u2514\u2500 ${relativeB}`);
|
|
156
|
+
expect(formatted).toContain(` \u2514\u2500 ${relativeC}`);
|
|
157
|
+
expect(formatted).toContain(` \u2514\u2500 ${relativeD}`);
|
|
158
|
+
expect(formatted).toContain(` \u2514\u2500 ${relativeE} \u2717 FAILED`);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
package/dist/import-vm.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { isBuiltin } from "node:module";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import vm from "node:vm";
|
|
5
5
|
import IM from "@import-maps/resolve";
|
|
6
|
+
import { CircularDependencyError, FileReadError } from "./error.mjs";
|
|
6
7
|
async function importBuiltinModule(specifier, context) {
|
|
7
8
|
const nodeModule = await import(specifier);
|
|
8
9
|
const keys = Object.keys(nodeModule);
|
|
@@ -47,29 +48,35 @@ export function createVmImport(baseURL, importMap = {}) {
|
|
|
47
48
|
}
|
|
48
49
|
const parsed = parse(specifier, parent);
|
|
49
50
|
if (moduleIds.includes(parsed.pathname)) {
|
|
50
|
-
throw new
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
throw new CircularDependencyError(
|
|
52
|
+
"Circular dependency detected",
|
|
53
|
+
moduleIds,
|
|
54
|
+
parsed.pathname
|
|
53
55
|
);
|
|
54
56
|
}
|
|
55
57
|
const module = cache.get(parsed.pathname);
|
|
56
58
|
if (module) {
|
|
57
59
|
return module;
|
|
58
60
|
}
|
|
59
|
-
const
|
|
61
|
+
const modulePromise = new Promise((resolve) => {
|
|
60
62
|
process.nextTick(() => {
|
|
61
63
|
moduleBuild().then(resolve);
|
|
62
64
|
});
|
|
63
65
|
});
|
|
64
66
|
const dirname = path.dirname(parsed.filename);
|
|
65
|
-
cache.set(parsed.pathname,
|
|
66
|
-
return
|
|
67
|
+
cache.set(parsed.pathname, modulePromise);
|
|
68
|
+
return modulePromise;
|
|
67
69
|
async function moduleBuild() {
|
|
68
70
|
let text;
|
|
69
71
|
try {
|
|
70
72
|
text = fs.readFileSync(parsed.pathname, "utf-8");
|
|
71
|
-
} catch {
|
|
72
|
-
throw new
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new FileReadError(
|
|
75
|
+
`Failed to read module: ${parsed.pathname}`,
|
|
76
|
+
moduleIds,
|
|
77
|
+
parsed.pathname,
|
|
78
|
+
error
|
|
79
|
+
);
|
|
73
80
|
}
|
|
74
81
|
const module2 = new vm.SourceTextModule(text, {
|
|
75
82
|
initializeImportMeta: (meta) => {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { createVmImport } from './import-vm';
|
|
2
2
|
export { createLoaderImport } from './import-loader';
|
|
3
3
|
export type { ImportMap, SpecifierMap, ScopesMap } from './types';
|
|
4
|
+
export { ModuleLoadingError, CircularDependencyError, FileReadError, formatCircularDependency, formatModuleChain } from './error';
|
package/dist/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@biomejs/biome": "1.9.4",
|
|
36
|
-
"@esmx/lint": "3.0.0-rc.
|
|
36
|
+
"@esmx/lint": "3.0.0-rc.31",
|
|
37
37
|
"@types/node": "^24.0.0",
|
|
38
38
|
"@vitest/coverage-v8": "3.2.4",
|
|
39
39
|
"stylelint": "16.21.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"unbuild": "3.5.0",
|
|
42
42
|
"vitest": "3.2.4"
|
|
43
43
|
},
|
|
44
|
-
"version": "3.0.0-rc.
|
|
44
|
+
"version": "3.0.0-rc.31",
|
|
45
45
|
"type": "module",
|
|
46
46
|
"private": false,
|
|
47
47
|
"exports": {
|
|
@@ -60,5 +60,5 @@
|
|
|
60
60
|
"template",
|
|
61
61
|
"public"
|
|
62
62
|
],
|
|
63
|
-
"gitHead": "
|
|
63
|
+
"gitHead": "d472904500f5d697cd61b99b012570f6b063c580"
|
|
64
64
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
CircularDependencyError,
|
|
5
|
+
FileReadError,
|
|
6
|
+
ModuleLoadingError,
|
|
7
|
+
formatCircularDependency,
|
|
8
|
+
formatModuleChain
|
|
9
|
+
} from './error';
|
|
10
|
+
|
|
11
|
+
describe('Module Loading Errors', () => {
|
|
12
|
+
let originalEnv: string | undefined;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
originalEnv = process.env.NO_COLOR;
|
|
16
|
+
process.env.NO_COLOR = '1'; // Disable colors for testing
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (originalEnv !== undefined) {
|
|
21
|
+
process.env.NO_COLOR = originalEnv;
|
|
22
|
+
} else {
|
|
23
|
+
process.env.NO_COLOR = undefined;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('CircularDependencyError', () => {
|
|
28
|
+
it('should create circular dependency error with correct properties', () => {
|
|
29
|
+
const moduleIds = ['/src/A.js', '/src/B.js'];
|
|
30
|
+
const targetModule = '/src/A.js';
|
|
31
|
+
|
|
32
|
+
const error = new CircularDependencyError(
|
|
33
|
+
'Test circular dependency',
|
|
34
|
+
moduleIds,
|
|
35
|
+
targetModule
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
expect(error.name).toBe('CircularDependencyError');
|
|
39
|
+
expect(error.message).toBe('Test circular dependency');
|
|
40
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
41
|
+
expect(error.targetModule).toBe(targetModule);
|
|
42
|
+
expect(error instanceof ModuleLoadingError).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should format circular dependency in toString', () => {
|
|
46
|
+
const moduleIds = ['/src/A.js', '/src/B.js'];
|
|
47
|
+
const targetModule = '/src/A.js';
|
|
48
|
+
|
|
49
|
+
const error = new CircularDependencyError(
|
|
50
|
+
'Circular dependency detected',
|
|
51
|
+
moduleIds,
|
|
52
|
+
targetModule
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const formatted = error.toString();
|
|
56
|
+
|
|
57
|
+
// Calculate expected relative paths
|
|
58
|
+
const relativeA = path.relative(process.cwd(), '/src/A.js');
|
|
59
|
+
const relativeB = path.relative(process.cwd(), '/src/B.js');
|
|
60
|
+
|
|
61
|
+
expect(formatted).toContain('CircularDependencyError');
|
|
62
|
+
expect(formatted).toContain('Circular dependency detected');
|
|
63
|
+
expect(formatted).toContain(`┌─ ${relativeA}`);
|
|
64
|
+
expect(formatted).toContain(`├─ ${relativeB}`);
|
|
65
|
+
expect(formatted).toContain(`└─ ${relativeA}`);
|
|
66
|
+
expect(formatted).toContain('circular');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('FileReadError', () => {
|
|
71
|
+
it('should create file read error with correct properties', () => {
|
|
72
|
+
const moduleIds = ['/src/main.js', '/src/components/App.js'];
|
|
73
|
+
const targetModule = '/src/missing.js';
|
|
74
|
+
const originalError = new Error(
|
|
75
|
+
'ENOENT: no such file or directory'
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const error = new FileReadError(
|
|
79
|
+
'Failed to read module',
|
|
80
|
+
moduleIds,
|
|
81
|
+
targetModule,
|
|
82
|
+
originalError
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(error.name).toBe('FileReadError');
|
|
86
|
+
expect(error.message).toBe('Failed to read module');
|
|
87
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
88
|
+
expect(error.targetModule).toBe(targetModule);
|
|
89
|
+
expect(error.originalError).toBe(originalError);
|
|
90
|
+
expect(error instanceof ModuleLoadingError).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should format module chain in toString', () => {
|
|
94
|
+
const moduleIds = ['/src/main.js', '/src/components/App.js'];
|
|
95
|
+
const targetModule = '/src/missing.js';
|
|
96
|
+
const originalError = new Error(
|
|
97
|
+
'ENOENT: no such file or directory'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const error = new FileReadError(
|
|
101
|
+
'Failed to read module',
|
|
102
|
+
moduleIds,
|
|
103
|
+
targetModule,
|
|
104
|
+
originalError
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const formatted = error.toString();
|
|
108
|
+
|
|
109
|
+
expect(formatted).toContain('FileReadError');
|
|
110
|
+
expect(formatted).toContain('Failed to read module');
|
|
111
|
+
expect(formatted).toContain('Import chain:');
|
|
112
|
+
expect(formatted).toContain('main.js');
|
|
113
|
+
expect(formatted).toContain('App.js');
|
|
114
|
+
expect(formatted).toContain('missing.js');
|
|
115
|
+
expect(formatted).toContain('✗ FAILED');
|
|
116
|
+
expect(formatted).toContain('Cause:');
|
|
117
|
+
expect(formatted).toContain('ENOENT');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Formatting Functions', () => {
|
|
122
|
+
it('should format circular dependency correctly', () => {
|
|
123
|
+
const moduleIds = ['/src/A.js', '/src/B.js', '/src/C.js'];
|
|
124
|
+
const targetModule = '/src/A.js';
|
|
125
|
+
|
|
126
|
+
const formatted = formatCircularDependency(moduleIds, targetModule);
|
|
127
|
+
|
|
128
|
+
// Calculate expected relative paths
|
|
129
|
+
const relativeA = path.relative(process.cwd(), '/src/A.js');
|
|
130
|
+
const relativeB = path.relative(process.cwd(), '/src/B.js');
|
|
131
|
+
const relativeC = path.relative(process.cwd(), '/src/C.js');
|
|
132
|
+
|
|
133
|
+
expect(formatted).toContain('Circular dependency:');
|
|
134
|
+
expect(formatted).toContain(`┌─ ${relativeA}`);
|
|
135
|
+
expect(formatted).toContain(`├─ ${relativeB}`);
|
|
136
|
+
expect(formatted).toContain(`├─ ${relativeC}`);
|
|
137
|
+
expect(formatted).toContain(`└─ ${relativeA}`);
|
|
138
|
+
expect(formatted).toContain('circular');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should format module chain correctly', () => {
|
|
142
|
+
const moduleIds = ['/src/main.js', '/src/app.js'];
|
|
143
|
+
const targetModule = '/src/missing.js';
|
|
144
|
+
const originalError = new Error('File not found');
|
|
145
|
+
|
|
146
|
+
const formatted = formatModuleChain(
|
|
147
|
+
moduleIds,
|
|
148
|
+
targetModule,
|
|
149
|
+
originalError
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(formatted).toContain('Import chain:');
|
|
153
|
+
expect(formatted).toContain('main.js');
|
|
154
|
+
expect(formatted).toContain('app.js');
|
|
155
|
+
expect(formatted).toContain('missing.js');
|
|
156
|
+
expect(formatted).toContain('✗ FAILED');
|
|
157
|
+
expect(formatted).toContain('Cause:');
|
|
158
|
+
expect(formatted).toContain('File not found');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should handle empty moduleIds in formatModuleChain', () => {
|
|
162
|
+
const moduleIds: string[] = [];
|
|
163
|
+
const targetModule = '/src/standalone.js';
|
|
164
|
+
|
|
165
|
+
const formatted = formatModuleChain(moduleIds, targetModule);
|
|
166
|
+
|
|
167
|
+
expect(formatted).toContain('Failed to load:');
|
|
168
|
+
expect(formatted).toContain('standalone.js');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle deep module chains', () => {
|
|
172
|
+
const moduleIds = [
|
|
173
|
+
'/src/a.js',
|
|
174
|
+
'/src/b.js',
|
|
175
|
+
'/src/c.js',
|
|
176
|
+
'/src/d.js'
|
|
177
|
+
];
|
|
178
|
+
const targetModule = '/src/e.js';
|
|
179
|
+
|
|
180
|
+
const formatted = formatModuleChain(moduleIds, targetModule);
|
|
181
|
+
|
|
182
|
+
// Calculate expected relative paths
|
|
183
|
+
const relativeA = path.relative(process.cwd(), '/src/a.js');
|
|
184
|
+
const relativeB = path.relative(process.cwd(), '/src/b.js');
|
|
185
|
+
const relativeC = path.relative(process.cwd(), '/src/c.js');
|
|
186
|
+
const relativeD = path.relative(process.cwd(), '/src/d.js');
|
|
187
|
+
const relativeE = path.relative(process.cwd(), '/src/e.js');
|
|
188
|
+
|
|
189
|
+
expect(formatted).toContain(relativeA);
|
|
190
|
+
expect(formatted).toContain(` └─ ${relativeB}`);
|
|
191
|
+
expect(formatted).toContain(` └─ ${relativeC}`);
|
|
192
|
+
expect(formatted).toContain(` └─ ${relativeD}`);
|
|
193
|
+
expect(formatted).toContain(` └─ ${relativeE} ✗ FAILED`);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
// Color constants for terminal output
|
|
4
|
+
const Colors = {
|
|
5
|
+
RED: '\x1b[31m',
|
|
6
|
+
YELLOW: '\x1b[33m',
|
|
7
|
+
CYAN: '\x1b[36m',
|
|
8
|
+
GRAY: '\x1b[90m',
|
|
9
|
+
RESET: '\x1b[0m',
|
|
10
|
+
BOLD: '\x1b[1m'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Check if terminal supports colors
|
|
14
|
+
const supportsColor = (): boolean => {
|
|
15
|
+
return (
|
|
16
|
+
!!(process.stdout?.isTTY && process.env.TERM !== 'dumb') ||
|
|
17
|
+
process.env.FORCE_COLOR === '1' ||
|
|
18
|
+
process.env.FORCE_COLOR === 'true'
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Color formatter utility
|
|
23
|
+
const useColors = supportsColor() && process.env.NO_COLOR !== '1';
|
|
24
|
+
|
|
25
|
+
const colorize = (text: string, color: string): string => {
|
|
26
|
+
return useColors ? `${color}${text}${Colors.RESET}` : text;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Get relative path from current working directory
|
|
30
|
+
const getRelativeFromCwd = (filePath: string): string => {
|
|
31
|
+
return path.relative(process.cwd(), filePath);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Formatting functions
|
|
35
|
+
export const formatCircularDependency = (
|
|
36
|
+
moduleIds: string[],
|
|
37
|
+
targetModule: string
|
|
38
|
+
): string => {
|
|
39
|
+
const fullChain = [...moduleIds, targetModule];
|
|
40
|
+
|
|
41
|
+
return `${colorize(colorize('Circular dependency:', Colors.BOLD), Colors.RED)}\n${fullChain
|
|
42
|
+
.map((module, index) => {
|
|
43
|
+
const isLastModule = index === fullChain.length - 1;
|
|
44
|
+
const prefix =
|
|
45
|
+
index === 0
|
|
46
|
+
? '┌─ '
|
|
47
|
+
: index === fullChain.length - 1
|
|
48
|
+
? '└─ '
|
|
49
|
+
: '├─ ';
|
|
50
|
+
|
|
51
|
+
const displayPath = getRelativeFromCwd(module);
|
|
52
|
+
|
|
53
|
+
// Check if this module appears elsewhere in the chain (circular dependency)
|
|
54
|
+
const isCircularModule =
|
|
55
|
+
fullChain.filter((m) => m === module).length > 1;
|
|
56
|
+
|
|
57
|
+
const coloredFile = isCircularModule
|
|
58
|
+
? colorize(colorize(displayPath, Colors.BOLD), Colors.RED)
|
|
59
|
+
: colorize(displayPath, Colors.CYAN);
|
|
60
|
+
|
|
61
|
+
const suffix = isLastModule
|
|
62
|
+
? ` ${colorize('← circular', Colors.YELLOW)}`
|
|
63
|
+
: '';
|
|
64
|
+
|
|
65
|
+
return `${colorize(prefix, Colors.GRAY)}${coloredFile}${suffix}`;
|
|
66
|
+
})
|
|
67
|
+
.join('\n')}`;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const formatModuleChain = (
|
|
71
|
+
moduleIds: string[],
|
|
72
|
+
targetModule: string,
|
|
73
|
+
originalError?: Error
|
|
74
|
+
): string => {
|
|
75
|
+
let result = '';
|
|
76
|
+
|
|
77
|
+
if (moduleIds.length === 0) {
|
|
78
|
+
const displayPath = getRelativeFromCwd(targetModule);
|
|
79
|
+
|
|
80
|
+
result = `${colorize('Failed to load:', Colors.CYAN)} ${colorize(displayPath, Colors.RED)}`;
|
|
81
|
+
} else {
|
|
82
|
+
const chain = [...moduleIds, targetModule];
|
|
83
|
+
result = `${colorize(colorize('Import chain:', Colors.BOLD), Colors.CYAN)}\n${chain
|
|
84
|
+
.map((module, index) => {
|
|
85
|
+
const indent = ' '.repeat(index);
|
|
86
|
+
const connector = index === 0 ? '' : '└─ ';
|
|
87
|
+
const displayPath = getRelativeFromCwd(module);
|
|
88
|
+
|
|
89
|
+
const isFailedFile = index === chain.length - 1;
|
|
90
|
+
const coloredFile = isFailedFile
|
|
91
|
+
? colorize(colorize(displayPath, Colors.BOLD), Colors.RED)
|
|
92
|
+
: colorize(displayPath, Colors.CYAN);
|
|
93
|
+
|
|
94
|
+
const status = isFailedFile
|
|
95
|
+
? ` ${colorize(colorize('✗ FAILED', Colors.BOLD), Colors.RED)}`
|
|
96
|
+
: '';
|
|
97
|
+
|
|
98
|
+
return `${colorize(indent + connector, Colors.GRAY)}${coloredFile}${status}`;
|
|
99
|
+
})
|
|
100
|
+
.join('\n')}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (originalError) {
|
|
104
|
+
result += `\n\n${colorize('Cause:', Colors.YELLOW)} ${originalError.message}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Base module loading error class
|
|
111
|
+
export class ModuleLoadingError extends Error {
|
|
112
|
+
constructor(
|
|
113
|
+
message: string,
|
|
114
|
+
public moduleIds: string[],
|
|
115
|
+
public targetModule: string,
|
|
116
|
+
public originalError?: Error
|
|
117
|
+
) {
|
|
118
|
+
super(message);
|
|
119
|
+
this.name = 'ModuleLoadingError';
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Circular dependency error class
|
|
124
|
+
export class CircularDependencyError extends ModuleLoadingError {
|
|
125
|
+
constructor(message: string, moduleIds: string[], targetModule: string) {
|
|
126
|
+
super(message, moduleIds, targetModule);
|
|
127
|
+
this.name = 'CircularDependencyError';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
toString(): string {
|
|
131
|
+
return `${this.name}: ${this.message}\n\n${formatCircularDependency(this.moduleIds, this.targetModule)}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// File read error class
|
|
136
|
+
export class FileReadError extends ModuleLoadingError {
|
|
137
|
+
constructor(
|
|
138
|
+
message: string,
|
|
139
|
+
moduleIds: string[],
|
|
140
|
+
targetModule: string,
|
|
141
|
+
originalError?: Error
|
|
142
|
+
) {
|
|
143
|
+
super(message, moduleIds, targetModule, originalError);
|
|
144
|
+
this.name = 'FileReadError';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
toString(): string {
|
|
148
|
+
return `${this.name}: ${this.message}\n\n${formatModuleChain(this.moduleIds, this.targetModule, this.originalError)}`;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/import-vm.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { isBuiltin } from 'node:module';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import vm from 'node:vm';
|
|
5
5
|
import IM from '@import-maps/resolve';
|
|
6
|
+
import { CircularDependencyError, FileReadError } from './error';
|
|
6
7
|
import type { ImportMap } from './types';
|
|
7
8
|
|
|
8
9
|
async function importBuiltinModule(specifier: string, context: vm.Context) {
|
|
@@ -58,8 +59,10 @@ export function createVmImport(baseURL: URL, importMap: ImportMap = {}) {
|
|
|
58
59
|
const parsed = parse(specifier, parent);
|
|
59
60
|
|
|
60
61
|
if (moduleIds.includes(parsed.pathname)) {
|
|
61
|
-
throw new
|
|
62
|
-
|
|
62
|
+
throw new CircularDependencyError(
|
|
63
|
+
'Circular dependency detected',
|
|
64
|
+
moduleIds,
|
|
65
|
+
parsed.pathname
|
|
63
66
|
);
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -67,22 +70,27 @@ export function createVmImport(baseURL: URL, importMap: ImportMap = {}) {
|
|
|
67
70
|
if (module) {
|
|
68
71
|
return module;
|
|
69
72
|
}
|
|
70
|
-
const
|
|
73
|
+
const modulePromise = new Promise<vm.SourceTextModule>((resolve) => {
|
|
71
74
|
process.nextTick(() => {
|
|
72
75
|
moduleBuild().then(resolve);
|
|
73
76
|
});
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
const dirname = path.dirname(parsed.filename);
|
|
77
|
-
cache.set(parsed.pathname,
|
|
78
|
-
return
|
|
80
|
+
cache.set(parsed.pathname, modulePromise);
|
|
81
|
+
return modulePromise;
|
|
79
82
|
|
|
80
83
|
async function moduleBuild(): Promise<vm.SourceTextModule> {
|
|
81
84
|
let text: string;
|
|
82
85
|
try {
|
|
83
86
|
text = fs.readFileSync(parsed.pathname, 'utf-8');
|
|
84
|
-
} catch {
|
|
85
|
-
throw new
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new FileReadError(
|
|
89
|
+
`Failed to read module: ${parsed.pathname}`,
|
|
90
|
+
moduleIds,
|
|
91
|
+
parsed.pathname,
|
|
92
|
+
error as Error
|
|
93
|
+
);
|
|
86
94
|
}
|
|
87
95
|
const module = new vm.SourceTextModule(text, {
|
|
88
96
|
initializeImportMeta: (meta) => {
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export { createVmImport } from './import-vm';
|
|
2
2
|
export { createLoaderImport } from './import-loader';
|
|
3
3
|
export type { ImportMap, SpecifierMap, ScopesMap } from './types';
|
|
4
|
+
export {
|
|
5
|
+
ModuleLoadingError,
|
|
6
|
+
CircularDependencyError,
|
|
7
|
+
FileReadError,
|
|
8
|
+
formatCircularDependency,
|
|
9
|
+
formatModuleChain
|
|
10
|
+
} from './error';
|