@esmx/import 3.0.0-rc.30 → 3.0.0-rc.32
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 +14 -0
- package/dist/error.mjs +104 -0
- package/dist/error.test.d.ts +1 -0
- package/dist/error.test.mjs +167 -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 +206 -0
- package/src/error.ts +172 -0
- package/src/import-vm.ts +15 -7
- package/src/index.ts +7 -0
package/dist/error.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
}
|
|
12
|
+
export declare class FileReadError extends ModuleLoadingError {
|
|
13
|
+
constructor(message: string, moduleIds: string[], targetModule: string, originalError?: Error);
|
|
14
|
+
}
|
package/dist/error.mjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
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("Module dependency chain (circular reference found):", 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("\u{1F504} Creates circular reference", 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("Module loading path:", 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("\u274C Loading 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("Error details:", 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
|
+
Object.defineProperty(this, "moduleIds", {
|
|
66
|
+
value: moduleIds,
|
|
67
|
+
writable: false,
|
|
68
|
+
enumerable: false,
|
|
69
|
+
configurable: true
|
|
70
|
+
});
|
|
71
|
+
Object.defineProperty(this, "targetModule", {
|
|
72
|
+
value: targetModule,
|
|
73
|
+
writable: false,
|
|
74
|
+
enumerable: false,
|
|
75
|
+
configurable: true
|
|
76
|
+
});
|
|
77
|
+
if (originalError) {
|
|
78
|
+
Object.defineProperty(this, "originalError", {
|
|
79
|
+
value: originalError,
|
|
80
|
+
writable: false,
|
|
81
|
+
enumerable: false,
|
|
82
|
+
configurable: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export class CircularDependencyError extends ModuleLoadingError {
|
|
88
|
+
constructor(message, moduleIds, targetModule) {
|
|
89
|
+
super(message, moduleIds, targetModule);
|
|
90
|
+
this.name = "CircularDependencyError";
|
|
91
|
+
this.stack = `${this.name}: ${message}
|
|
92
|
+
|
|
93
|
+
${formatCircularDependency(moduleIds, targetModule)}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export class FileReadError extends ModuleLoadingError {
|
|
97
|
+
constructor(message, moduleIds, targetModule, originalError) {
|
|
98
|
+
super(message, moduleIds, targetModule, originalError);
|
|
99
|
+
this.name = "FileReadError";
|
|
100
|
+
this.stack = `${this.name}: ${message}
|
|
101
|
+
|
|
102
|
+
${formatModuleChain(moduleIds, targetModule, originalError)}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
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.stack).toContain("Test circular dependency");
|
|
35
|
+
expect(error.stack).toContain(
|
|
36
|
+
"Module dependency chain (circular reference found):"
|
|
37
|
+
);
|
|
38
|
+
expect(error.stack).toContain("\u250C\u2500");
|
|
39
|
+
expect(error.stack).toContain("\u2514\u2500");
|
|
40
|
+
expect(error.stack).toContain("\u{1F504} Creates circular reference");
|
|
41
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
42
|
+
expect(error.targetModule).toBe(targetModule);
|
|
43
|
+
expect(error instanceof ModuleLoadingError).toBe(true);
|
|
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
|
+
const error = new CircularDependencyError(
|
|
49
|
+
"Circular dependency detected",
|
|
50
|
+
moduleIds,
|
|
51
|
+
targetModule
|
|
52
|
+
);
|
|
53
|
+
const formatted = error.toString();
|
|
54
|
+
expect(formatted).toContain("CircularDependencyError");
|
|
55
|
+
expect(formatted).toContain("Circular dependency detected");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("FileReadError", () => {
|
|
59
|
+
it("should create file read error with correct properties", () => {
|
|
60
|
+
const moduleIds = ["/src/main.js", "/src/components/App.js"];
|
|
61
|
+
const targetModule = "/src/missing.js";
|
|
62
|
+
const originalError = new Error(
|
|
63
|
+
"ENOENT: no such file or directory"
|
|
64
|
+
);
|
|
65
|
+
const error = new FileReadError(
|
|
66
|
+
"Failed to read module",
|
|
67
|
+
moduleIds,
|
|
68
|
+
targetModule,
|
|
69
|
+
originalError
|
|
70
|
+
);
|
|
71
|
+
expect(error.name).toBe("FileReadError");
|
|
72
|
+
expect(error.message).toBe("Failed to read module");
|
|
73
|
+
expect(error.stack).toContain("Failed to read module");
|
|
74
|
+
expect(error.stack).toContain("Module loading path:");
|
|
75
|
+
expect(error.stack).toContain("main.js");
|
|
76
|
+
expect(error.stack).toContain("App.js");
|
|
77
|
+
expect(error.stack).toContain("missing.js");
|
|
78
|
+
expect(error.stack).toContain("\u274C Loading failed");
|
|
79
|
+
expect(error.stack).toContain("Error details:");
|
|
80
|
+
expect(error.stack).toContain("ENOENT");
|
|
81
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
82
|
+
expect(error.targetModule).toBe(targetModule);
|
|
83
|
+
expect(error.originalError).toBe(originalError);
|
|
84
|
+
expect(error instanceof ModuleLoadingError).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
it("should format module chain in toString", () => {
|
|
87
|
+
const moduleIds = ["/src/main.js", "/src/components/App.js"];
|
|
88
|
+
const targetModule = "/src/missing.js";
|
|
89
|
+
const originalError = new Error(
|
|
90
|
+
"ENOENT: no such file or directory"
|
|
91
|
+
);
|
|
92
|
+
const error = new FileReadError(
|
|
93
|
+
"Failed to read module",
|
|
94
|
+
moduleIds,
|
|
95
|
+
targetModule,
|
|
96
|
+
originalError
|
|
97
|
+
);
|
|
98
|
+
const formatted = error.toString();
|
|
99
|
+
expect(formatted).toContain("FileReadError");
|
|
100
|
+
expect(formatted).toContain("Failed to read module");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("Formatting Functions", () => {
|
|
104
|
+
it("should format circular dependency correctly", () => {
|
|
105
|
+
const moduleIds = ["/src/A.js", "/src/B.js", "/src/C.js"];
|
|
106
|
+
const targetModule = "/src/A.js";
|
|
107
|
+
const formatted = formatCircularDependency(moduleIds, targetModule);
|
|
108
|
+
const relativeA = path.relative(process.cwd(), "/src/A.js");
|
|
109
|
+
const relativeB = path.relative(process.cwd(), "/src/B.js");
|
|
110
|
+
const relativeC = path.relative(process.cwd(), "/src/C.js");
|
|
111
|
+
expect(formatted).toContain(
|
|
112
|
+
"Module dependency chain (circular reference found):"
|
|
113
|
+
);
|
|
114
|
+
expect(formatted).toContain(`\u250C\u2500 ${relativeA}`);
|
|
115
|
+
expect(formatted).toContain(`\u251C\u2500 ${relativeB}`);
|
|
116
|
+
expect(formatted).toContain(`\u251C\u2500 ${relativeC}`);
|
|
117
|
+
expect(formatted).toContain(`\u2514\u2500 ${relativeA}`);
|
|
118
|
+
expect(formatted).toContain("\u{1F504} Creates circular reference");
|
|
119
|
+
});
|
|
120
|
+
it("should format module chain correctly", () => {
|
|
121
|
+
const moduleIds = ["/src/main.js", "/src/app.js"];
|
|
122
|
+
const targetModule = "/src/missing.js";
|
|
123
|
+
const originalError = new Error("File not found");
|
|
124
|
+
const formatted = formatModuleChain(
|
|
125
|
+
moduleIds,
|
|
126
|
+
targetModule,
|
|
127
|
+
originalError
|
|
128
|
+
);
|
|
129
|
+
expect(formatted).toContain("Module loading path:");
|
|
130
|
+
expect(formatted).toContain("main.js");
|
|
131
|
+
expect(formatted).toContain("app.js");
|
|
132
|
+
expect(formatted).toContain("missing.js");
|
|
133
|
+
expect(formatted).toContain("\u274C Loading failed");
|
|
134
|
+
expect(formatted).toContain("Error details:");
|
|
135
|
+
expect(formatted).toContain("File not found");
|
|
136
|
+
});
|
|
137
|
+
it("should handle empty moduleIds in formatModuleChain", () => {
|
|
138
|
+
const moduleIds = [];
|
|
139
|
+
const targetModule = "/src/standalone.js";
|
|
140
|
+
const formatted = formatModuleChain(moduleIds, targetModule);
|
|
141
|
+
expect(formatted).toContain("Failed to load:");
|
|
142
|
+
expect(formatted).toContain("standalone.js");
|
|
143
|
+
});
|
|
144
|
+
it("should handle deep module chains", () => {
|
|
145
|
+
const moduleIds = [
|
|
146
|
+
"/src/a.js",
|
|
147
|
+
"/src/b.js",
|
|
148
|
+
"/src/c.js",
|
|
149
|
+
"/src/d.js"
|
|
150
|
+
];
|
|
151
|
+
const targetModule = "/src/e.js";
|
|
152
|
+
const formatted = formatModuleChain(moduleIds, targetModule);
|
|
153
|
+
const relativeA = path.relative(process.cwd(), "/src/a.js");
|
|
154
|
+
const relativeB = path.relative(process.cwd(), "/src/b.js");
|
|
155
|
+
const relativeC = path.relative(process.cwd(), "/src/c.js");
|
|
156
|
+
const relativeD = path.relative(process.cwd(), "/src/d.js");
|
|
157
|
+
const relativeE = path.relative(process.cwd(), "/src/e.js");
|
|
158
|
+
expect(formatted).toContain(relativeA);
|
|
159
|
+
expect(formatted).toContain(` \u2514\u2500 ${relativeB}`);
|
|
160
|
+
expect(formatted).toContain(` \u2514\u2500 ${relativeC}`);
|
|
161
|
+
expect(formatted).toContain(` \u2514\u2500 ${relativeD}`);
|
|
162
|
+
expect(formatted).toContain(
|
|
163
|
+
` \u2514\u2500 ${relativeE} \u274C Loading failed`
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
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.32",
|
|
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.32",
|
|
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": "8f7c52dde97c2cf74a783e578328cc72bde78c3b"
|
|
64
64
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
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
|
+
// Message is now clean and simple
|
|
40
|
+
expect(error.message).toBe('Test circular dependency');
|
|
41
|
+
// Formatted content is in stack property
|
|
42
|
+
expect(error.stack).toContain('Test circular dependency');
|
|
43
|
+
expect(error.stack).toContain(
|
|
44
|
+
'Module dependency chain (circular reference found):'
|
|
45
|
+
);
|
|
46
|
+
expect(error.stack).toContain('┌─');
|
|
47
|
+
expect(error.stack).toContain('└─');
|
|
48
|
+
expect(error.stack).toContain('🔄 Creates circular reference');
|
|
49
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
50
|
+
expect(error.targetModule).toBe(targetModule);
|
|
51
|
+
expect(error instanceof ModuleLoadingError).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should format circular dependency in toString', () => {
|
|
55
|
+
const moduleIds = ['/src/A.js', '/src/B.js'];
|
|
56
|
+
const targetModule = '/src/A.js';
|
|
57
|
+
|
|
58
|
+
const error = new CircularDependencyError(
|
|
59
|
+
'Circular dependency detected',
|
|
60
|
+
moduleIds,
|
|
61
|
+
targetModule
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const formatted = error.toString();
|
|
65
|
+
|
|
66
|
+
expect(formatted).toContain('CircularDependencyError');
|
|
67
|
+
expect(formatted).toContain('Circular dependency detected');
|
|
68
|
+
// toString() now uses default Error behavior, so it only shows name and message
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('FileReadError', () => {
|
|
73
|
+
it('should create file read error with correct properties', () => {
|
|
74
|
+
const moduleIds = ['/src/main.js', '/src/components/App.js'];
|
|
75
|
+
const targetModule = '/src/missing.js';
|
|
76
|
+
const originalError = new Error(
|
|
77
|
+
'ENOENT: no such file or directory'
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const error = new FileReadError(
|
|
81
|
+
'Failed to read module',
|
|
82
|
+
moduleIds,
|
|
83
|
+
targetModule,
|
|
84
|
+
originalError
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(error.name).toBe('FileReadError');
|
|
88
|
+
// Message is now clean and simple
|
|
89
|
+
expect(error.message).toBe('Failed to read module');
|
|
90
|
+
// Formatted content is in stack property
|
|
91
|
+
expect(error.stack).toContain('Failed to read module');
|
|
92
|
+
expect(error.stack).toContain('Module loading path:');
|
|
93
|
+
expect(error.stack).toContain('main.js');
|
|
94
|
+
expect(error.stack).toContain('App.js');
|
|
95
|
+
expect(error.stack).toContain('missing.js');
|
|
96
|
+
expect(error.stack).toContain('❌ Loading failed');
|
|
97
|
+
expect(error.stack).toContain('Error details:');
|
|
98
|
+
expect(error.stack).toContain('ENOENT');
|
|
99
|
+
expect(error.moduleIds).toEqual(moduleIds);
|
|
100
|
+
expect(error.targetModule).toBe(targetModule);
|
|
101
|
+
expect(error.originalError).toBe(originalError);
|
|
102
|
+
expect(error instanceof ModuleLoadingError).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should format module chain in toString', () => {
|
|
106
|
+
const moduleIds = ['/src/main.js', '/src/components/App.js'];
|
|
107
|
+
const targetModule = '/src/missing.js';
|
|
108
|
+
const originalError = new Error(
|
|
109
|
+
'ENOENT: no such file or directory'
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const error = new FileReadError(
|
|
113
|
+
'Failed to read module',
|
|
114
|
+
moduleIds,
|
|
115
|
+
targetModule,
|
|
116
|
+
originalError
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const formatted = error.toString();
|
|
120
|
+
|
|
121
|
+
expect(formatted).toContain('FileReadError');
|
|
122
|
+
expect(formatted).toContain('Failed to read module');
|
|
123
|
+
// toString() now uses default Error behavior, so it only shows name and message
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('Formatting Functions', () => {
|
|
128
|
+
it('should format circular dependency correctly', () => {
|
|
129
|
+
const moduleIds = ['/src/A.js', '/src/B.js', '/src/C.js'];
|
|
130
|
+
const targetModule = '/src/A.js';
|
|
131
|
+
|
|
132
|
+
const formatted = formatCircularDependency(moduleIds, targetModule);
|
|
133
|
+
|
|
134
|
+
// Calculate expected relative paths
|
|
135
|
+
const relativeA = path.relative(process.cwd(), '/src/A.js');
|
|
136
|
+
const relativeB = path.relative(process.cwd(), '/src/B.js');
|
|
137
|
+
const relativeC = path.relative(process.cwd(), '/src/C.js');
|
|
138
|
+
|
|
139
|
+
expect(formatted).toContain(
|
|
140
|
+
'Module dependency chain (circular reference found):'
|
|
141
|
+
);
|
|
142
|
+
expect(formatted).toContain(`┌─ ${relativeA}`);
|
|
143
|
+
expect(formatted).toContain(`├─ ${relativeB}`);
|
|
144
|
+
expect(formatted).toContain(`├─ ${relativeC}`);
|
|
145
|
+
expect(formatted).toContain(`└─ ${relativeA}`);
|
|
146
|
+
expect(formatted).toContain('🔄 Creates circular reference');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should format module chain correctly', () => {
|
|
150
|
+
const moduleIds = ['/src/main.js', '/src/app.js'];
|
|
151
|
+
const targetModule = '/src/missing.js';
|
|
152
|
+
const originalError = new Error('File not found');
|
|
153
|
+
|
|
154
|
+
const formatted = formatModuleChain(
|
|
155
|
+
moduleIds,
|
|
156
|
+
targetModule,
|
|
157
|
+
originalError
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(formatted).toContain('Module loading path:');
|
|
161
|
+
expect(formatted).toContain('main.js');
|
|
162
|
+
expect(formatted).toContain('app.js');
|
|
163
|
+
expect(formatted).toContain('missing.js');
|
|
164
|
+
expect(formatted).toContain('❌ Loading failed');
|
|
165
|
+
expect(formatted).toContain('Error details:');
|
|
166
|
+
expect(formatted).toContain('File not found');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should handle empty moduleIds in formatModuleChain', () => {
|
|
170
|
+
const moduleIds: string[] = [];
|
|
171
|
+
const targetModule = '/src/standalone.js';
|
|
172
|
+
|
|
173
|
+
const formatted = formatModuleChain(moduleIds, targetModule);
|
|
174
|
+
|
|
175
|
+
expect(formatted).toContain('Failed to load:');
|
|
176
|
+
expect(formatted).toContain('standalone.js');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle deep module chains', () => {
|
|
180
|
+
const moduleIds = [
|
|
181
|
+
'/src/a.js',
|
|
182
|
+
'/src/b.js',
|
|
183
|
+
'/src/c.js',
|
|
184
|
+
'/src/d.js'
|
|
185
|
+
];
|
|
186
|
+
const targetModule = '/src/e.js';
|
|
187
|
+
|
|
188
|
+
const formatted = formatModuleChain(moduleIds, targetModule);
|
|
189
|
+
|
|
190
|
+
// Calculate expected relative paths
|
|
191
|
+
const relativeA = path.relative(process.cwd(), '/src/a.js');
|
|
192
|
+
const relativeB = path.relative(process.cwd(), '/src/b.js');
|
|
193
|
+
const relativeC = path.relative(process.cwd(), '/src/c.js');
|
|
194
|
+
const relativeD = path.relative(process.cwd(), '/src/d.js');
|
|
195
|
+
const relativeE = path.relative(process.cwd(), '/src/e.js');
|
|
196
|
+
|
|
197
|
+
expect(formatted).toContain(relativeA);
|
|
198
|
+
expect(formatted).toContain(` └─ ${relativeB}`);
|
|
199
|
+
expect(formatted).toContain(` └─ ${relativeC}`);
|
|
200
|
+
expect(formatted).toContain(` └─ ${relativeD}`);
|
|
201
|
+
expect(formatted).toContain(
|
|
202
|
+
` └─ ${relativeE} ❌ Loading failed`
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
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('Module dependency chain (circular reference found):', 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('🔄 Creates circular reference', 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('Module loading path:', 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('❌ Loading 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('Error details:', 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
|
+
// Hide auxiliary properties from enumeration to avoid cluttering error display
|
|
122
|
+
Object.defineProperty(this, 'moduleIds', {
|
|
123
|
+
value: moduleIds,
|
|
124
|
+
writable: false,
|
|
125
|
+
enumerable: false,
|
|
126
|
+
configurable: true
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
Object.defineProperty(this, 'targetModule', {
|
|
130
|
+
value: targetModule,
|
|
131
|
+
writable: false,
|
|
132
|
+
enumerable: false,
|
|
133
|
+
configurable: true
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (originalError) {
|
|
137
|
+
Object.defineProperty(this, 'originalError', {
|
|
138
|
+
value: originalError,
|
|
139
|
+
writable: false,
|
|
140
|
+
enumerable: false,
|
|
141
|
+
configurable: true
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Circular dependency error class
|
|
148
|
+
export class CircularDependencyError extends ModuleLoadingError {
|
|
149
|
+
constructor(message: string, moduleIds: string[], targetModule: string) {
|
|
150
|
+
super(message, moduleIds, targetModule);
|
|
151
|
+
this.name = 'CircularDependencyError';
|
|
152
|
+
|
|
153
|
+
// Custom stack for clean error display
|
|
154
|
+
this.stack = `${this.name}: ${message}\n\n${formatCircularDependency(moduleIds, targetModule)}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// File read error class
|
|
159
|
+
export class FileReadError extends ModuleLoadingError {
|
|
160
|
+
constructor(
|
|
161
|
+
message: string,
|
|
162
|
+
moduleIds: string[],
|
|
163
|
+
targetModule: string,
|
|
164
|
+
originalError?: Error
|
|
165
|
+
) {
|
|
166
|
+
super(message, moduleIds, targetModule, originalError);
|
|
167
|
+
this.name = 'FileReadError';
|
|
168
|
+
|
|
169
|
+
// Custom stack for clean error display
|
|
170
|
+
this.stack = `${this.name}: ${message}\n\n${formatModuleChain(moduleIds, targetModule, originalError)}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
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';
|