@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.
@@ -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
+ });
@@ -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 RangeError(
51
- `Module circular reference:
52
- ${JSON.stringify([...moduleIds, parsed.pathname], null, 4)}`
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 pe = new Promise((resolve) => {
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, pe);
66
- return pe;
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 Error(`Failed to read module: ${parsed.pathname}`);
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
@@ -1,2 +1,9 @@
1
1
  export { createVmImport } from "./import-vm.mjs";
2
2
  export { createLoaderImport } from "./import-loader.mjs";
3
+ export {
4
+ ModuleLoadingError,
5
+ CircularDependencyError,
6
+ FileReadError,
7
+ formatCircularDependency,
8
+ formatModuleChain
9
+ } from "./error.mjs";
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.30",
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.30",
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": "0bc6815e4a805e552e67a245201a4a75599f58f5"
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 RangeError(
62
- `Module circular reference: \n ${JSON.stringify([...moduleIds, parsed.pathname], null, 4)}`
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 pe = new Promise<vm.SourceTextModule>((resolve) => {
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, pe);
78
- return pe;
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 Error(`Failed to read module: ${parsed.pathname}`);
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';