Skip to content

Commit eb42c85

Browse files
authored
Merge pull request #950 from d-zero-dev/worktree-proud-tickling-rose
Adopt Explicit Resource Management for using/dispose lifecycle
2 parents 1321d85 + f4034d6 commit eb42c85

80 files changed

Lines changed: 1691 additions & 433 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/@d-zero/a11y-check-axe-scenario/package.json‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"publishConfig": {
88
"access": "public"
99
},
10+
"engines": {
11+
"node": ">=24.11.0"
12+
},
1013
"type": "module",
1114
"exports": {
1215
".": {
@@ -30,11 +33,11 @@
3033
},
3134
"devDependencies": {
3235
"axe-core": "4.12.1",
33-
"puppeteer": "25.3.0"
36+
"puppeteer": "25.5.0"
3437
},
3538
"peerDependencies": {
3639
"axe-core": "4.12.1",
37-
"puppeteer": "25.2.1"
40+
"puppeteer": "25.5.0"
3841
},
3942
"repository": {
4043
"type": "git",

‎packages/@d-zero/a11y-check-core/package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"publishConfig": {
88
"access": "public"
99
},
10+
"engines": {
11+
"node": ">=24.11.0"
12+
},
1013
"type": "module",
1114
"exports": {
1215
".": {
@@ -27,7 +30,7 @@
2730
"@d-zero/shared": "0.22.5",
2831
"ansi-colors": "4.1.3",
2932
"color-contrast-checker": "2.1.0",
30-
"puppeteer": "25.3.0"
33+
"puppeteer": "25.5.0"
3134
},
3235
"devDependencies": {
3336
"@d-zero/dealer": "1.10.4",

‎packages/@d-zero/a11y-check-scenarios/package.json‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"publishConfig": {
88
"access": "public"
99
},
10+
"engines": {
11+
"node": ">=24.11.0"
12+
},
1013
"type": "module",
1114
"exports": {
1215
".": {
@@ -28,10 +31,10 @@
2831
"ansi-colors": "4.1.3"
2932
},
3033
"devDependencies": {
31-
"puppeteer": "25.3.0"
34+
"puppeteer": "25.5.0"
3235
},
3336
"peerDependencies": {
34-
"puppeteer": "25.2.1"
37+
"puppeteer": "25.5.0"
3538
},
3639
"repository": {
3740
"type": "git",

‎packages/@d-zero/a11y-check-scenarios/src/scenario2.ts‎

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { ScenarioOptions } from './types.js';
22
import type { NeedAnalysis } from '@d-zero/a11y-check-core';
3+
import type { ConsoleMessage } from 'puppeteer';
34

45
import { createScenario } from '@d-zero/a11y-check-core';
56
import { Cache } from '@d-zero/shared/cache';
7+
import { disposableListener } from '@d-zero/shared/disposable-listener';
68
import c from 'ansi-colors';
79

810
const scenarioId = 'a11y-check/scenario02';
@@ -41,19 +43,27 @@ export default createScenario((options?: ScenarioOptions) => {
4143
for (const selector of navigations) {
4244
const logBase = `Finding "${selector}"`;
4345
logger(`Finding "${selector}"`);
44-
page.on('console', (msg) => {
45-
const msgType = msg.type();
46-
switch (msgType) {
47-
case 'error': {
48-
logger(`${logBase}: ${c.red(msg.text())}`);
49-
break;
46+
// `using` により、各ループ反復の終わりで確実にリスナーが解除される。
47+
// ループ内で page.on('console', ...) するため、解除しないと
48+
// selector の数だけリスナーが累積する。
49+
using _consoleListener = disposableListener(
50+
page,
51+
'console',
52+
(msg: ConsoleMessage) => {
53+
const msgType = msg.type();
54+
switch (msgType) {
55+
case 'error': {
56+
logger(`${logBase}: ${c.red(msg.text())}`);
57+
break;
58+
}
59+
default: {
60+
logger(`${logBase}: ${c.gray(msg.text())}`);
61+
break;
62+
}
5063
}
51-
default: {
52-
logger(`${logBase}: ${c.gray(msg.text())}`);
53-
break;
54-
}
55-
}
56-
});
64+
},
65+
);
66+
void _consoleListener;
5767
const outerHTML = await page.evaluate((selector) => {
5868
return [...document.querySelectorAll(selector)].map((el) => el.outerHTML);
5969
}, selector);

‎packages/@d-zero/a11y-check/package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"publishConfig": {
88
"access": "public"
99
},
10+
"engines": {
11+
"node": ">=24.11.0"
12+
},
1013
"type": "module",
1114
"exports": {
1215
".": {

‎packages/@d-zero/a11y-check/src/spreadsheet.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,23 @@ export class SpreadsheetReporter {
1616
// eslint-disable-next-line no-restricted-syntax
1717
private constructor() {}
1818

19+
/**
20+
* `await using` 宣言のスコープ脱出時に呼ばれ、内部の {@link SheetTable} を
21+
* フラッシュする。バッファに未送信行が残ったままスコープを抜けてデータが
22+
* 欠損するのを防ぐ。
23+
* @example
24+
* ```ts
25+
* {
26+
* await using reporter = await SpreadsheetReporter.setup(sheetUrl, sheetName);
27+
* await reporter.report(violations);
28+
* } // スコープ脱出時に自動で内部 SheetTable の未送信バッファが flush される
29+
* ```
30+
*/
31+
async [Symbol.asyncDispose]() {
32+
if (this.#table) {
33+
await this.#table[Symbol.asyncDispose]();
34+
}
35+
}
1936
async report(results: readonly Violation[]) {
2037
if (!this.#table) {
2138
throw new Error('Table is not created');

‎packages/@d-zero/anatomist/package.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
"publishConfig": {
88
"access": "public"
99
},
10+
"engines": {
11+
"node": ">=24.11.0"
12+
},
1013
"type": "module",
1114
"exports": {
1215
".": {
@@ -36,10 +39,11 @@
3639
},
3740
"dependencies": {
3841
"@d-zero/beholder": "4.2.2",
42+
"@d-zero/cli-core": "1.3.16",
3943
"@d-zero/dealer": "1.10.4",
4044
"@d-zero/puppeteer-page-scan": "4.6.8",
4145
"@d-zero/shared": "0.22.5",
42-
"puppeteer": "25.3.0"
46+
"puppeteer": "25.5.0"
4347
},
4448
"repository": {
4549
"type": "git",

‎packages/@d-zero/anatomist/src/cli.spec.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ describe('runCli', () => {
215215
return true;
216216
}),
217217
end: mockEnd,
218+
// cli.ts の `await using outFile` が要求する Symbol.asyncDispose のスタブ
219+
[Symbol.asyncDispose]: vi.fn(async () => {}),
218220
} as never);
219221
vi.mocked(runBatch).mockImplementation((_urls, options?: RunBatchOptions) => {
220222
options?.onResult?.({
@@ -249,6 +251,7 @@ describe('runCli', () => {
249251
end: vi.fn((callback: (error: Error) => void) => {
250252
callback(new Error('disk full'));
251253
}),
254+
[Symbol.asyncDispose]: vi.fn(async () => {}),
252255
} as never);
253256

254257
const stderr = new PassThrough();

‎packages/@d-zero/anatomist/src/cli.ts‎

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { createWriteStream } from 'node:fs';
88
import { readFile } from 'node:fs/promises';
99
import process from 'node:process';
1010

11+
import { unwrapSuppressedError } from '@d-zero/cli-core';
12+
1113
import { formatResultLine } from './format-output.js';
1214
import { parseArgs } from './parse-args.js';
1315
import { parseUrlList } from './parse-url-list.js';
@@ -134,9 +136,12 @@ export async function runCli(options: {
134136
return 1;
135137
}
136138

137-
const outStream: NodeJS.WritableStream = args.out
138-
? createWriteStream(args.out)
139-
: options.stdout;
139+
// `await using` により、runBatch() が想定外の例外を投げてスコープを抜けても
140+
// --out で開いたファイル記述子が確実に閉じられる(stdout の場合は outFile が
141+
// undefined のままなので dispose は no-op — process.stdout を誤って
142+
// close してしまうことはない)。
143+
await using outFile = args.out ? createWriteStream(args.out) : undefined;
144+
const outStream: NodeJS.WritableStream = outFile ?? options.stdout;
140145

141146
let hadError = false;
142147
await runBatch(urls, {
@@ -158,24 +163,26 @@ export async function runCli(options: {
158163
},
159164
onError: (url, error) => {
160165
hadError = true;
161-
options.stderr.write(
162-
`anatomist: failed to analyze ${url}: ${(error as Error).message}\n`,
163-
);
166+
// SuppressedError(using スコープ内で本体と dispose の両方が例外を投げた
167+
// 場合)を分解し、定型メッセージの裏に隠れる根本原因を両方とも出力する
168+
for (const cause of unwrapSuppressedError(error)) {
169+
options.stderr.write(
170+
`anatomist: failed to analyze ${url}: ${cause instanceof Error ? cause.message : String(cause)}\n`,
171+
);
172+
}
164173
},
165174
});
166175

167-
if (args.out) {
176+
if (outFile) {
168177
try {
169178
await new Promise<void>((resolve, reject) => {
170-
(outStream as ReturnType<typeof createWriteStream>).end(
171-
(error?: Error | null) => {
172-
if (error) {
173-
reject(error);
174-
} else {
175-
resolve();
176-
}
177-
},
178-
);
179+
outFile.end((error?: Error | null) => {
180+
if (error) {
181+
reject(error);
182+
} else {
183+
resolve();
184+
}
185+
});
179186
});
180187
} catch (error) {
181188
// Consistent with every other failure path here: report to stderr

‎packages/@d-zero/anatomist/src/run-batch.spec.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,37 @@ function makeSequentialDealMock() {
4545
}
4646

4747
describe('runBatch', () => {
48-
let mockPage: { close: ReturnType<typeof vi.fn> };
49-
let mockBrowser: { newPage: ReturnType<typeof vi.fn>; close: ReturnType<typeof vi.fn> };
48+
let mockPage: {
49+
close: ReturnType<typeof vi.fn>;
50+
[Symbol.asyncDispose]: ReturnType<typeof vi.fn>;
51+
};
52+
let mockBrowser: {
53+
newPage: ReturnType<typeof vi.fn>;
54+
close: ReturnType<typeof vi.fn>;
55+
[Symbol.asyncDispose]: ReturnType<typeof vi.fn>;
56+
};
5057

5158
beforeEach(() => {
52-
mockPage = { close: vi.fn().mockResolvedValue() };
59+
// `run-batch.ts` now uses `await using`, which requires a real
60+
// `Symbol.asyncDispose` implementation — delegate to the existing
61+
// `close` mock so assertions on `close` call counts stay meaningful.
62+
mockPage = {
63+
close: vi.fn().mockResolvedValue(),
64+
[Symbol.asyncDispose]: vi.fn(),
65+
};
66+
mockPage[Symbol.asyncDispose].mockImplementation(async () => {
67+
await mockPage.close();
68+
});
69+
5370
mockBrowser = {
5471
newPage: vi.fn().mockResolvedValue(mockPage),
5572
close: vi.fn().mockResolvedValue(),
73+
[Symbol.asyncDispose]: vi.fn(),
5674
};
75+
mockBrowser[Symbol.asyncDispose].mockImplementation(async () => {
76+
await mockBrowser.close();
77+
});
78+
5779
vi.mocked(launch).mockResolvedValue(mockBrowser as never);
5880
vi.mocked(deal).mockImplementation(makeSequentialDealMock() as never);
5981
vi.mocked(analyzePageLayout).mockReset().mockResolvedValue([]);

0 commit comments

Comments
 (0)