Skip to content

Commit ccde465

Browse files
committedSep 13, 2023
fix(watch): deep watch files in Node v20 (#310)
1 parent 84d722d commit ccde465

File tree

5 files changed

+118
-119
lines changed

5 files changed

+118
-119
lines changed
 

‎.nvmrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
v18.13.0
1+
v20.6.1

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"dependencies": {
4848
"@esbuild-kit/cjs-loader": "^2.4.2",
4949
"@esbuild-kit/core-utils": "^3.3.0",
50-
"@esbuild-kit/esm-loader": "^2.6.1"
50+
"@esbuild-kit/esm-loader": "^2.6.3"
5151
},
5252
"optionalDependencies": {
5353
"fsevents": "~2.3.2"

‎pnpm-lock.yaml

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎tests/specs/watch.ts

+111-113
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
1+
import { type Readable } from 'node:stream';
12
import path from 'path';
23
import { setTimeout } from 'timers/promises';
4+
import { on } from 'events';
35
import { testSuite, expect } from 'manten';
46
import { createFixture } from 'fs-fixture';
57
import { tsx } from '../utils/tsx';
68

9+
type MaybePromise<T> = T | Promise<T>;
10+
const interact = async (
11+
stdout: Readable,
12+
actions: ((data: string) => MaybePromise<boolean | void>)[],
13+
) => {
14+
let currentAction = actions.shift();
15+
16+
const buffers: Buffer[] = [];
17+
while (currentAction) {
18+
for await (const [chunk] of on(stdout, 'data')) {
19+
buffers.push(chunk);
20+
if (await currentAction(chunk.toString())) {
21+
currentAction = actions.shift();
22+
break;
23+
}
24+
}
25+
}
26+
27+
return Buffer.concat(buffers).toString();
28+
};
29+
730
export default testSuite(async ({ describe }, fixturePath: string) => {
831
describe('watch', ({ test, describe }) => {
932
test('require file path', async () => {
@@ -15,10 +38,17 @@ export default testSuite(async ({ describe }, fixturePath: string) => {
1538
});
1639

1740
test('watch files for changes', async ({ onTestFinish }) => {
41+
let initialValue = Date.now();
1842
const fixture = await createFixture({
19-
'index.js': 'console.log(1)',
43+
'package.json': JSON.stringify({
44+
type: 'module',
45+
}),
46+
'index.js': `
47+
import { value } from './value.js';
48+
console.log(value);
49+
`,
50+
'value.js': `export const value = ${initialValue};`,
2051
});
21-
2252
onTestFinish(async () => await fixture.rm());
2353

2454
const tsxProcess = tsx({
@@ -28,21 +58,23 @@ export default testSuite(async ({ describe }, fixturePath: string) => {
2858
],
2959
});
3060

31-
await new Promise<void>((resolve) => {
32-
async function onStdOut(data: Buffer) {
33-
const chunkString = data.toString();
61+
await interact(
62+
tsxProcess.stdout!,
63+
[
64+
async (data) => {
65+
if (data.includes(`${initialValue}\n`)) {
66+
initialValue = Date.now();
67+
await fixture.writeFile('value.js', `export const value = ${initialValue};`);
68+
return true;
69+
}
70+
},
71+
data => data.includes(`${initialValue}\n`),
72+
],
73+
);
3474

35-
if (chunkString.match('1\n')) {
36-
await fixture.writeFile('index.js', 'console.log(2)');
37-
} else if (chunkString.match('2\n')) {
38-
tsxProcess.kill();
39-
resolve();
40-
}
41-
}
75+
tsxProcess.kill();
4276

43-
tsxProcess.stdout!.on('data', onStdOut);
44-
tsxProcess.stderr!.on('data', onStdOut);
45-
});
77+
await tsxProcess;
4678
}, 10_000);
4779

4880
test('suppresses warnings & clear screen', async () => {
@@ -53,32 +85,24 @@ export default testSuite(async ({ describe }, fixturePath: string) => {
5385
],
5486
});
5587

56-
const stdout = await new Promise<string>((resolve) => {
57-
let aggregateStdout = '';
58-
let hitEnter = false;
59-
60-
function onStdOut(data: Buffer) {
61-
const chunkString = data.toString();
62-
// console.log({ chunkString });
63-
64-
aggregateStdout += chunkString;
65-
66-
if (chunkString.match('log-argv.ts')) {
67-
if (hitEnter) {
68-
tsxProcess.kill();
69-
resolve(aggregateStdout);
70-
} else {
71-
hitEnter = true;
88+
await interact(
89+
tsxProcess.stdout!,
90+
[
91+
(data) => {
92+
if (data.includes('log-argv.ts')) {
7293
tsxProcess.stdin?.write('enter');
94+
return true;
7395
}
74-
}
75-
}
76-
tsxProcess.stdout!.on('data', onStdOut);
77-
tsxProcess.stderr!.on('data', onStdOut);
78-
});
96+
},
97+
data => data.includes('log-argv.ts'),
98+
],
99+
);
100+
101+
tsxProcess.kill();
79102

80-
expect(stdout).not.toMatch('Warning');
81-
expect(stdout).toMatch('\u001Bc');
103+
const { all } = await tsxProcess;
104+
expect(all).not.toMatch('Warning');
105+
expect(all).toMatch('\u001Bc');
82106
}, 10_000);
83107

84108
test('passes flags', async () => {
@@ -90,20 +114,15 @@ export default testSuite(async ({ describe }, fixturePath: string) => {
90114
],
91115
});
92116

93-
const stdout = await new Promise<string>((resolve) => {
94-
tsxProcess.stdout!.on('data', (chunk) => {
95-
const chunkString = chunk.toString();
96-
if (chunkString.startsWith('[')) {
97-
resolve(chunkString);
98-
}
99-
});
100-
});
117+
await interact(
118+
tsxProcess.stdout!,
119+
[data => data.startsWith('["')],
120+
);
101121

102122
tsxProcess.kill();
103123

104-
expect(stdout).toMatch('"--some-flag"');
105-
106-
await tsxProcess;
124+
const { all } = await tsxProcess;
125+
expect(all).toMatch('"--some-flag"');
107126
}, 10_000);
108127

109128
test('wait for exit', async ({ onTestFinish }) => {
@@ -130,42 +149,23 @@ export default testSuite(async ({ describe }, fixturePath: string) => {
130149
],
131150
});
132151

133-
const stdout = await new Promise<string>((resolve) => {
134-
const buffers: Buffer[] = [];
135-
const waitingOn: [string, (() => void)][] = [
136-
['start\n', () => {
137-
tsxProcess.stdin?.write('enter');
138-
}],
139-
['end\n', () => {}],
140-
];
141-
142-
let currentWaitingOn = waitingOn.shift();
143-
async function onStdOut(data: Buffer) {
144-
buffers.push(data);
145-
const chunkString = data.toString();
146-
147-
if (currentWaitingOn) {
148-
const [expected, callback] = currentWaitingOn!;
149-
150-
// eslint-disable-next-line unicorn/prefer-regexp-test
151-
if (chunkString.match(expected)) {
152-
callback();
153-
currentWaitingOn = waitingOn.shift();
154-
if (!currentWaitingOn) {
155-
tsxProcess.kill();
156-
resolve(Buffer.concat(buffers).toString());
157-
}
152+
await interact(
153+
tsxProcess.stdout!,
154+
[
155+
(data) => {
156+
if (data.includes('start\n')) {
157+
tsxProcess.stdin?.write('enter');
158+
return true;
158159
}
159-
}
160-
}
161-
162-
tsxProcess.stdout!.on('data', onStdOut);
163-
tsxProcess.stderr!.on('data', onStdOut);
164-
});
160+
},
161+
data => data.includes('end\n'),
162+
],
163+
);
165164

166-
expect(stdout).toMatch(/start[\s\S]+end/);
165+
tsxProcess.kill();
167166

168-
await tsxProcess;
167+
const { all } = await tsxProcess;
168+
expect(all).toMatch(/start[\s\S]+end/);
169169
}, 10_000);
170170

171171
describe('help', ({ test }) => {
@@ -188,20 +188,15 @@ export default testSuite(async ({ describe }, fixturePath: string) => {
188188
],
189189
});
190190

191-
const stdout = await new Promise<string>((resolve) => {
192-
tsxProcess.stdout!.on('data', (chunk) => {
193-
const chunkString = chunk.toString();
194-
if (chunkString.startsWith('[')) {
195-
resolve(chunkString);
196-
}
197-
});
198-
});
191+
await interact(
192+
tsxProcess.stdout!,
193+
[data => data.startsWith('["')],
194+
);
199195

200196
tsxProcess.kill();
201197

202-
expect(stdout).toMatch('"--help"');
203-
204-
await tsxProcess;
198+
const { all } = await tsxProcess;
199+
expect(all).toMatch('"--help"');
205200
}, 5000);
206201
});
207202

@@ -235,28 +230,31 @@ export default testSuite(async ({ describe }, fixturePath: string) => {
235230
],
236231
});
237232

238-
tsxProcess.stdout!.on('data', async (data: Buffer) => {
239-
const chunkString = data.toString();
240-
if (chunkString === `${value} ${value}\n`) {
241-
value = Date.now();
242-
await Promise.all([
243-
fixture.writeFile(fileA, `export default ${value}`),
244-
fixture.writeFile(fileB, `export default ${value}`),
245-
]);
246-
247-
await setTimeout(500);
248-
await fixture.writeFile(entryFile, 'console.log("TERMINATE")');
249-
}
250-
251-
if (chunkString === 'TERMINATE\n') {
252-
tsxProcess.kill();
253-
}
254-
});
233+
await interact(
234+
tsxProcess.stdout!,
235+
[
236+
async (data) => {
237+
if (data === `${value} ${value}\n`) {
238+
value = Date.now();
239+
await Promise.all([
240+
fixture.writeFile(fileA, `export default ${value}`),
241+
fixture.writeFile(fileB, `export default ${value}`),
242+
]);
243+
244+
await setTimeout(500);
245+
await fixture.writeFile(entryFile, 'console.log("TERMINATE")');
246+
return true;
247+
}
248+
},
249+
data => data === 'TERMINATE\n',
250+
],
251+
);
255252

256-
const tsxProcessResolved = await tsxProcess;
253+
tsxProcess.kill();
257254

258-
expect(tsxProcessResolved.stdout).not.toMatch(`${value} ${value}`);
259-
expect(tsxProcessResolved.stderr).toBe('');
255+
const { all, stderr } = await tsxProcess;
256+
expect(all).not.toMatch(`${value} ${value}`);
257+
expect(stderr).toBe('');
260258
}, 10_000);
261259
});
262260
});

‎tests/utils/tsx.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const tsx = (
2525
nodeOptions: [],
2626
cwd: options.cwd,
2727
reject: false,
28+
all: true,
2829
},
2930
);
3031

0 commit comments

Comments
 (0)
Please sign in to comment.