@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.
@@ -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
+ });
@@ -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.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.30",
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": "0bc6815e4a805e552e67a245201a4a75599f58f5"
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 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';