Skip to content

Commit 6d757ef

Browse files
authored
Make it work on Windows (#748)
1 parent c6ab34c commit 6d757ef

File tree

13 files changed

+98
-39
lines changed

13 files changed

+98
-39
lines changed

.changeset/tiny-years-tie.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'pleasantest': minor
3+
---
4+
5+
Reimplement windows support
6+
7+
Long ago, Pleasantest worked on Windows, but without regular testing it gradually diverged. This release adds proper Windows support back and adds automated testing for it.

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ concurrency:
1212

1313
jobs:
1414
test:
15-
runs-on: ubuntu-latest
15+
runs-on: ${{ matrix.platform }}
1616
strategy:
17+
fail-fast: false
1718
matrix:
1819
node-version: [18.x, 20.x, 22.x]
20+
platform: [ubuntu-latest, windows-latest]
1921
steps:
2022
- uses: actions/checkout@v4
2123
- name: Use Node.js ${{ matrix.node-version }}

package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,15 @@
117117
},
118118
"main": "./dist/cjs/index.cjs",
119119
"exports": {
120-
"require": {
121-
".": "./dist/cjs/index.cjs",
122-
"types": "./dist/index.d.cts"
123-
},
124-
"import": {
125-
".": "./dist/esm/index.mjs",
126-
"types": "./dist/index.d.mts"
120+
".": {
121+
"require": {
122+
"default": "./dist/cjs/index.cjs",
123+
"types": "./dist/index.d.cts"
124+
},
125+
"import": {
126+
"default": "./dist/esm/index.mjs",
127+
"types": "./dist/index.d.mts"
128+
}
127129
}
128130
},
129131
"types": "./dist/index.d.ts",

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const withBrowser: WithBrowser = (...args: any[]) => {
124124
// ignore if it is the current file
125125
if (stackItem === thisFile) return false;
126126
// ignore if it is an internal-to-node thing
127-
if (!stackItem.startsWith('/')) return false;
127+
if (stackItem.startsWith('node:')) return false;
128128
// Find the first item that is not the current file
129129
return true;
130130
});

src/jest-dom/rollup.config.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
import * as childProcess from 'node:child_process';
2-
import { promisify } from 'node:util';
1+
import { fork } from 'node:child_process';
32

43
import babel from '@rollup/plugin-babel';
54
import nodeResolve from '@rollup/plugin-node-resolve';
65
import terser from '@rollup/plugin-terser';
76

87
import { rollupPluginDomAccessibilityApi } from '../rollup-plugin-dom-accessibility-api.js';
98

10-
const exec = promisify(childProcess.exec);
11-
129
const extensions = ['.js', '.jsx', '.es6', '.es', '.mjs', '.ts', '.tsx'];
1310

14-
const { stdout, stderr } = await exec('./node_modules/.bin/patch-package');
15-
process.stdout.write(stdout);
16-
process.stderr.write(stderr);
11+
await new Promise((resolve, reject) => {
12+
const child = fork('./node_modules/patch-package/index.js', [], {
13+
stdio: 'inherit',
14+
});
15+
child.on('exit', (code) => {
16+
if (code === 0) resolve();
17+
else reject(new Error(`patch-package exited with code ${code}`));
18+
});
19+
});
1720

1821
const stubs = {
1922
chalk: `

src/module-server/bundle-npm-module.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ export const bundleNpmModule = async (
7070
},
7171
load(id) {
7272
if (id === virtualEntry) {
73-
const code = `export * from '${mod}'
74-
export {${namedExports.join(', ')}} from '${mod}'
75-
export { default } from '${mod}'`;
76-
return code;
73+
const modNameString = JSON.stringify(mod);
74+
return `export * from ${modNameString}
75+
export {${namedExports.join(', ')}} from ${modNameString}
76+
export { default } from ${modNameString}`;
7777
}
7878
},
7979
} as Plugin),

src/module-server/extensions-and-detection.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { isAbsolute } from 'node:path';
12
export const npmPrefix = '@npm/';
23

34
export const isRelativeOrAbsoluteImport = (id: string) =>
45
id === '.' ||
56
id === '..' ||
67
id.startsWith('./') ||
78
id.startsWith('../') ||
8-
id.startsWith('/');
9+
isAbsolute(id);
910

1011
export const isBareImport = (id: string) =>
1112
!(

src/module-server/middleware/js.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { promises as fs } from 'node:fs';
2-
import { dirname, posix, relative, resolve, sep } from 'node:path';
2+
import { dirname, isAbsolute, posix, relative, resolve, sep } from 'node:path';
33

44
import type { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping';
55
import MagicString from 'magic-string';
@@ -102,7 +102,8 @@ export const jsMiddleware = async ({
102102
import.meta.pleasantestArgs = [...window._pleasantestArgs]
103103
}`;
104104
const fileSrc = await fs.readFile(file, 'utf8');
105-
const inlineStartIdx = fileSrc.indexOf(code);
105+
const EOL = detectLineEndings(fileSrc);
106+
const inlineStartIdx = fileSrc.indexOf(code.replaceAll('\n', EOL));
106107
code = injectedArgsCode + code;
107108
if (inlineStartIdx !== -1) {
108109
const str = new MagicString(fileSrc);
@@ -180,16 +181,16 @@ export const jsMiddleware = async ({
180181
if (resolved) {
181182
spec = typeof resolved === 'object' ? resolved.id : resolved;
182183
if (spec.startsWith('@npm/')) return addBuildId(`/${spec}`);
183-
if (/^(\/|\\|[a-z]:\\)/i.test(spec)) {
184+
if (isAbsolute(path)) {
184185
// Change FS-absolute paths to relative
185-
spec = relative(dirname(file), spec).split(sep).join(posix.sep);
186+
spec = relative(dirname(file), spec).split(sep).join('/');
186187
if (!/^\.?\.?\//.test(spec)) spec = `./${spec}`;
187188
}
188189

189190
if (typeof resolved === 'object' && resolved.external) {
190191
if (/^(data|https?):/.test(spec)) return spec;
191192

192-
spec = relative(root, spec).split(sep).join(posix.sep);
193+
spec = relative(root, spec).split(sep).join('/');
193194
if (!/^(\/|[\w-]+:)/.test(spec)) spec = `/${spec}`;
194195
return addBuildId(spec);
195196
}
@@ -233,3 +234,13 @@ export const jsMiddleware = async ({
233234
}
234235
};
235236
};
237+
238+
const detectLineEndings = (fileSrc: string) => {
239+
// Find the first line end (\n) and check if the character before is \r
240+
// This tells us whether the file uses \r\n or just \n for line endings.
241+
// Using node:os.EOL is not sufficient because git on windows
242+
// Can be configured to check out files with either kind of line ending.
243+
const firstLineEndPos = fileSrc.indexOf('\n');
244+
if (fileSrc[firstLineEndPos - 1] === '\r') return '\r\n';
245+
return '\n';
246+
};

src/module-server/node-resolve.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,22 @@ const createFs = async (input: string) => {
4141
);
4242

4343
/** Replaces all instances of randomized tmp dir with "." */
44-
const unrandomizePath = (text: string) => text.split(dir).join('.');
44+
const unrandomizePath = (text: string) => text.replaceAll(dir, '.');
4545

4646
const resolve = async (id: string, { from }: { from?: string } = {}) => {
4747
const result = await nodeResolve(
4848
id,
4949
join(dir, from || 'index.js'),
5050
dir,
5151
).catch((error) => {
52-
throw changeErrorMessage(error, (error) => unrandomizePath(error));
52+
throw changeErrorMessage(error, (error) =>
53+
unrandomizePath(error).replaceAll(sep, '/'),
54+
);
5355
});
5456
if (result)
55-
return unrandomizePath(typeof result === 'string' ? result : result.path);
57+
return unrandomizePath(
58+
typeof result === 'string' ? result : result.path,
59+
).replaceAll(sep, '/');
5660
};
5761

5862
return { resolve };

src/module-server/node-resolve.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { promises as fs } from 'node:fs';
2-
import { dirname, join, resolve as pResolve, posix, relative } from 'node:path';
2+
import { dirname, join, resolve as pResolve, relative, sep } from 'node:path';
33

44
import { resolve, legacy as resolveLegacy } from 'resolve.exports';
55

@@ -129,7 +129,8 @@ export const resolveFromNodeModules = async (
129129
const cacheKey = resolveCacheKey(id, importer, root);
130130
const cached = resolveCache.get(cacheKey);
131131
if (cached) return cached;
132-
const pathChunks = id.split(posix.sep);
132+
// Split the path up into chunks based on either kind of slash
133+
const pathChunks = id.split(/[/\\]/g);
133134
const isNpmNamespace = id[0] === '@';
134135
// If it is an npm namespace, then get the first two folders, otherwise just one
135136
const packageName = pathChunks.slice(0, isNpmNamespace ? 2 : 1);
@@ -161,7 +162,10 @@ export const resolveFromNodeModules = async (
161162
}
162163

163164
const pkgJson = await readPkgJson(pkgDir);
164-
const main = readMainFields(pkgJson, subPath, true);
165+
// Main/exports fields in package.json are defined with forward slashes.
166+
// On windows, subPath will have \ instead of /, but we need to change it
167+
// to match what will be listed in the package.json.
168+
const main = readMainFields(pkgJson, subPath.replaceAll(sep, '/'), true);
165169
let result;
166170
if (main) result = join(pkgDir, main);
167171
else if (!('exports' in pkgJson)) {

0 commit comments

Comments
 (0)