Skip to content

Commit 9995d68

Browse files
authored
Merge pull request #168 from gadget-inc/allow-dots-in-dirnames
Support dots in directory names when running with esm: true
2 parents 5c1cc0c + 7bec77a commit 9995d68

File tree

9 files changed

+82
-29
lines changed

9 files changed

+82
-29
lines changed

integration-test/reload/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "1.0.0",
44
"description": "",
55
"main": "index.js",
6+
"type": "module",
67
"scripts": {
78
"test": "echo \"Error: no test specified\" && exit 1"
89
},

integration-test/server/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "1.0.0",
44
"description": "",
55
"main": "index.js",
6+
"type": "module",
67
"scripts": {
78
"test": "echo \"Error: no test specified\" && exit 1"
89
},

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "wds",
3-
"version": "0.21.0",
3+
"version": "0.21.1",
44
"author": "Harry Brundage",
55
"license": "MIT",
66
"bin": {

spec/Supervisor.spec.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,17 @@ import { expect, test } from "vitest";
77
const dirname = fileURLToPath(new URL(".", import.meta.url));
88

99
const childExit = (child: ChildProcess) => {
10-
return new Promise<void>((resolve) => {
10+
return new Promise<void>((resolve, reject) => {
11+
child.on("error", (err: Error) => {
12+
reject(err);
13+
});
14+
1115
child.on("exit", (code: number) => {
12-
resolve();
13-
expect(code).toEqual(0);
16+
if (code == 0) {
17+
resolve();
18+
} else {
19+
reject(new Error(`Child process exited with code ${code}`));
20+
}
1421
});
1522
});
1623
};
@@ -111,3 +118,15 @@ test("it doesn't have any stdin if wds is started with terminal commands", async
111118

112119
expect(output).toEqual("");
113120
}, 10000);
121+
122+
test("it can load a commonjs module inside a directory that contains a dot when in esm mode", async () => {
123+
const binPath = path.join(dirname, "../pkg/wds.bin.js");
124+
const scriptPath = path.join(dirname, "fixtures/esm/github.com/wds/simple.ts");
125+
126+
const child = spawn("node", [binPath, scriptPath], {
127+
stdio: ["inherit", "inherit", "inherit"],
128+
env: process.env,
129+
});
130+
131+
await childExit(child);
132+
}, 10000);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("success");

spec/fixtures/esm/package.json

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "wds-test-sources",
3+
"version": "0.0.1",
4+
"license": "MIT"
5+
}

spec/fixtures/esm/wds.js

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = {
2+
esm: true,
3+
swc: {
4+
jsc: {
5+
parser: {
6+
syntax: "typescript",
7+
decorators: true,
8+
dynamicImport: true,
9+
},
10+
target: "es2020",
11+
},
12+
},
13+
};

spec/fixtures/src/wds.js

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
module.exports = {
2-
"swc": {
3-
"jsc": {
4-
"parser": {
5-
"syntax": "typescript",
6-
"decorators": true,
7-
"dynamicImport": true
2+
swc: {
3+
jsc: {
4+
parser: {
5+
syntax: "typescript",
6+
decorators: true,
7+
dynamicImport: true,
88
},
9-
"target": "es2020"
9+
target: "es2020",
10+
},
11+
module: {
12+
type: "commonjs",
13+
strictMode: true,
14+
lazy: true,
1015
},
11-
"module": {
12-
"type": "commonjs",
13-
"strictMode": true,
14-
"lazy": true
15-
}
1616
},
17-
}
17+
};

src/hooks/child-process-esm-loader.ts

+25-12
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import fs from "node:fs/promises";
55
import type { LoadHook, ModuleFormat, ResolveHook } from "node:module";
66
import { builtinModules, createRequire } from "node:module";
7-
import { dirname, extname, join, resolve as resolvePath } from "node:path";
7+
import { dirname, join, resolve as resolvePath } from "node:path";
88
import { fileURLToPath, pathToFileURL } from "node:url";
99
import { ResolverFactory } from "oxc-resolver";
1010
import { debugLog } from "../SyncWorker.cjs";
@@ -166,7 +166,7 @@ export const load: LoadHook = async function load(url, context, nextLoad) {
166166
return await nextLoad(url, context);
167167
}
168168

169-
const format: ModuleFormat = context.format ?? (await getPackageType(url)) ?? "commonjs";
169+
const format: ModuleFormat = context.format ?? (await getPackageType(fileURLToPath(url))) ?? "commonjs";
170170
if (format == "commonjs") {
171171
// if the package is a commonjs package and we return the source contents explicitly, this loader will process the inner requires, but with a broken/different version of \`require\` internally.
172172
// if we return a nullish source, node falls back to the old, mainline require chain, which has require.cache set properly and whatnot.
@@ -197,30 +197,43 @@ export const load: LoadHook = async function load(url, context, nextLoad) {
197197
};
198198
};
199199

200-
async function getPackageType(url: string): Promise<ModuleFormat | undefined> {
201-
// `url` is only a file path during the first iteration when passed the resolved url from the load() hook
202-
// an actual file path from load() will contain a file extension as it's required by the spec
203-
// this simple truthy check for whether `url` contains a file extension will work for most projects but does not cover some edge-cases (such as extensionless files or a url ending in a trailing space) extensionless files or a url ending in a trailing space)
204-
const isFilePath = !!extname(url);
200+
async function getPackageType(path: string, isFilePath?: boolean): Promise<ModuleFormat | undefined> {
201+
try {
202+
isFilePath ??= await fs.readdir(path).then(() => false);
203+
} catch (err: any) {
204+
if (err?.code !== "ENOTDIR") {
205+
throw err;
206+
}
207+
isFilePath = true;
208+
}
209+
205210
// If it is a file path, get the directory it's in
206-
const dir = isFilePath ? dirname(fileURLToPath(url)) : url;
211+
const dir = isFilePath ? dirname(path) : path;
207212
// Compose a file path to a package.json in the same directory,
208213
// which may or may not exist
209214
const packagePath = resolvePath(dir, "package.json");
210-
debugLog?.("getPackageType", { url, packagePath });
215+
debugLog?.("getPackageType", { path, packagePath });
211216

212217
// Try to read the possibly nonexistent package.json
213218
const type = await fs
214219
.readFile(packagePath, { encoding: "utf8" })
215-
.then((filestring) => JSON.parse(filestring).type)
220+
.then((filestring) => {
221+
// As per node's docs, we use the nearest package.json to figure out the package type (see https://nodejs.org/api/packages.html#type)
222+
// If it lacks a "type" key, we assume "commonjs". If we fail to parse, we also choose to assume "commonjs".
223+
try {
224+
return JSON.parse(filestring).type || "commonjs";
225+
} catch (_err) {
226+
return "commonjs";
227+
}
228+
})
216229
.catch((err) => {
217230
if (err?.code !== "ENOENT") console.error(err);
218231
});
219-
// If package.json existed and contained a `type` field with a value, voilà
232+
// If package.json existed, we guarantee a type and return it.
220233
if (type) return type;
221234
// Otherwise, (if not at the root) continue checking the next directory up
222235
// If at the root, stop and return false
223-
if (dir.length > 1) return await getPackageType(resolvePath(dir, ".."));
236+
if (dir.length > 1) return await getPackageType(resolvePath(dir, ".."), false);
224237

225238
return undefined;
226239
}

0 commit comments

Comments
 (0)