feedback5: merge R6 (tooling) — correctness-only ESLint, a first checkJs slice + core typedefs, tools/check-imports.mjs, tools/package.mjs (+ --lite), CI tooling job

This commit is contained in:
sganggs committed 2026-10-05 16:34:01 +08:00
commit 3288afc787
14 files changed
+2174 -7

No files matched your search

+110
View File
@@ -0,0 +1,110 @@
// Boundary checker: fixture rules, plus the real tree's current violation list.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { findSpecifiers, scan } from '../tools/check-imports.mjs';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
function fixture(files) {
const root = mkdtempSync(join(tmpdir(), 'sp-imp-'));
for (const [rel, body] of Object.entries(files)) {
const abs = join(root, rel);
mkdirSync(dirname(abs), { recursive: true });
writeFileSync(abs, body);
}
return root;
}
test('comments are not imports, and a multiline import is', () => {
const specs = findSpecifiers(`
// import fs from 'node:fs'
/* require('node:path') */
import {
a,
b,
} from './tier1.js';
export function fromThing() { return 1; }
`);
assert.deepEqual(specs.map((s) => s.spec), ['./tier1.js']);
});
test('sim and match rules on a fixture tree', () => {
const root = fixture({
'server/sim/clean.js': `
// import fs from 'node:fs'
import { a } from './other.js';
export function load() { return 1; }
export { a };
`,
'server/sim/bad.js': `
import '../match/Match.js';
import '../lobby.js';
import '../index.js';
import '../net.js';
import '../../public/js/main.js';
import fs from 'node:fs';
import 'fs';
export { z } from '../match/board.js';
const p = await import('node:path');
`,
'server/match/ok.js': `
import '../sim/rng.js';
import 'node:fs';
import '../data.js';
`,
'server/match/bad.js': `
import '../../public/js/main.js';
import 'node:http';
import 'ws';
import '../net.js';
import '../index.js';
`,
});
try {
const keys = scan(root).violations.map((v) => `${v.file} ${v.spec} ${v.code}`).sort();
assert.deepEqual(keys, [
'server/match/bad.js ../../public/js/main.js match-client',
'server/match/bad.js ../index.js match-http',
'server/match/bad.js ../net.js match-http',
'server/match/bad.js node:http match-http',
'server/match/bad.js ws match-http',
'server/sim/bad.js ../../public/js/main.js sim-client',
'server/sim/bad.js ../index.js sim-entry',
'server/sim/bad.js ../lobby.js sim-lobby',
'server/sim/bad.js ../match/Match.js sim-match',
'server/sim/bad.js ../match/board.js sim-match',
'server/sim/bad.js ../net.js sim-http',
'server/sim/bad.js fs sim-node',
'server/sim/bad.js node:fs sim-node',
'server/sim/bad.js node:path sim-node',
]);
assert.equal(scan(root).notes.length, 0);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test('this repo: only the Node data loader crosses the sim boundary', () => {
const { violations, notes } = scan(ROOT);
assert.deepEqual(violations.map((v) => `${v.file}:${v.line}: ${v.spec} [${v.code}]`), [
'server/sim/nodeData.js:11: node:fs [sim-node]',
'server/sim/nodeData.js:12: node:path [sim-node]',
'server/sim/nodeData.js:13: node:url [sim-node]',
]);
assert.ok(notes.some((n) => n.file === 'server/sim/nodeData.js' && n.resolved === 'server/data.js'));
assert.ok(notes.every((n) => n.resolved === 'server/data.js'));
});
test('the CLI lists hits and fails only with --strict', () => {
const loose = spawnSync(process.execPath, ['tools/check-imports.mjs'], { cwd: ROOT, encoding: 'utf8' });
assert.equal(loose.status, 0);
assert.match(loose.stdout, /server\/sim\/nodeData\.js:11: node:fs \[sim-node\]/);
const strict = spawnSync(process.execPath, ['tools/check-imports.mjs', '--strict'], { cwd: ROOT, encoding: 'utf8' });
assert.equal(strict.status, 1);
assert.match(strict.stdout, /node:fs/);
});
+155
View File
@@ -0,0 +1,155 @@
// Player-package file selection. Dry-run only: no zip, no npm ci.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
extraPaths,
isExcluded,
packageOutIsUnsafe,
selectTracked,
textHasHomePath,
trackedFiles,
} from '../tools/package.mjs';
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const SAMPLE = [
'server/index.js',
'server/sim/rng.js',
'shared/constants.js',
'public/index.html',
'public/js/main.js',
'public/vendor/preact.mjs',
'public/dev/game-mock.html',
'public/assets/board.png',
'public/fonts/fonts.css',
'data/chess.json',
'data/local-assets.json',
'tools/setup.mjs',
'tools/vendor.mjs',
'tools/fetch-assets.mjs',
'tools/assets/plan.mjs',
'tools/local-extract/extract.py',
'tools/golden.mjs',
'tools/check-imports.mjs',
'scripts/start.sh',
'docs/PLAYING.md',
'docs/DEPLOY.md',
'docs/WINDOWS.md',
'docs/ASSETS.md',
'docs/img/combat.jpg',
'docs/DESIGN.md',
'docs/SIM.md',
'docs/research/03-operators.json',
'docs/research/10-networking-hosting.md',
'test/version.test.js',
'package.json',
'package-lock.json',
'LICENSE',
'NOTICE.md',
'THIRD-PARTY-NOTICES.md',
'README.md',
'CHANGELOG.md',
'Dockerfile',
'.dockerignore',
'AGENTS.md',
'.env',
'.env.local',
'server/.env.production',
'pv/clip.mp4',
'3,9,11回合情况/note.txt',
'handoff/HANDOFF.md',
'node_modules/ws/index.js',
'.cache/x',
'eslint.config.js',
'jsconfig.json',
'types/core.js',
];
test('selectTracked keeps runtime files and drops the rest', () => {
const got = selectTracked(SAMPLE);
assert.deepEqual(got, selectTracked(SAMPLE, { lite: true }));
for (const rel of [
'server/index.js', 'tools/setup.mjs', 'tools/vendor.mjs', 'docs/PLAYING.md',
'docs/DEPLOY.md', 'public/index.html', 'data/chess.json', 'package.json',
'Dockerfile', 'scripts/start.sh', 'tools/assets/plan.mjs', 'tools/local-extract/extract.py',
]) assert.ok(got.includes(rel), rel);
for (const rel of [
'tools/golden.mjs', 'tools/check-imports.mjs', 'test/version.test.js', 'public/dev/game-mock.html',
'public/assets/board.png', 'public/fonts/fonts.css', 'docs/research/10-networking-hosting.md',
'docs/research/03-operators.json', 'docs/DESIGN.md', 'docs/SIM.md', 'AGENTS.md', '.env',
'.env.local', 'server/.env.production', 'pv/clip.mp4', '3,9,11回合情况/note.txt',
'handoff/HANDOFF.md', 'node_modules/ws/index.js', 'data/local-assets.json', 'eslint.config.js',
'types/core.js',
]) assert.ok(!got.includes(rel), rel);
assert.equal(isExcluded('docs/research/10-networking-hosting.md'), true);
});
test('lite extras skip art, fonts and the local manifest', () => {
assert.deepEqual(extraPaths({ lite: true, hasLocalManifest: true }), ['public/vendor']);
assert.deepEqual(extraPaths({ lite: false, hasLocalManifest: true }), [
'public/vendor', 'public/assets', 'public/fonts', 'data/local-assets.json',
]);
assert.deepEqual(extraPaths({ lite: false, hasLocalManifest: false }), [
'public/vendor', 'public/assets', 'public/fonts',
]);
});
test('home-directory text and unsafe output directories', () => {
assert.equal(textHasHomePath('see /Users/example/game'), 'mac-users');
assert.equal(textHasHomePath('C:\\Users\\example\\game'), 'win-users');
assert.equal(textHasHomePath('prefix /home/example/game'), 'posix-home');
assert.equal(textHasHomePath('https://github.com/example/game'), null);
assert.equal(packageOutIsUnsafe(ROOT, ROOT), true);
assert.equal(packageOutIsUnsafe(path.dirname(ROOT), ROOT), true);
assert.equal(packageOutIsUnsafe(path.join(ROOT, 'dist-pack'), ROOT), false);
});
test('real git ls-files: runtime in, dev and private out', () => {
const got = new Set(selectTracked(trackedFiles(ROOT)));
assert.ok(got.has('server/index.js'));
assert.ok(got.has('tools/setup.mjs'));
assert.ok(got.has('docs/PLAYING.md'));
assert.ok(got.has('package-lock.json'));
assert.equal(got.has('tools/golden.mjs'), false);
assert.equal(got.has('tools/package.mjs'), false);
assert.equal(got.has('test/golden.test.js'), false);
assert.equal(got.has('docs/DESIGN.md'), false);
assert.equal(got.has('eslint.config.js'), false);
for (const rel of got) {
assert.equal(rel.startsWith('docs/research/'), false, rel);
assert.equal(rel.startsWith('test/'), false, rel);
assert.equal(rel.startsWith('public/dev/'), false, rel);
assert.equal(rel.startsWith('handoff/'), false, rel);
}
});
test('dry-run lists the lite set and prints both size estimates', () => {
const lite = spawnSync(process.execPath, ['tools/package.mjs', '--dry-run', '--lite'], {
cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
});
assert.equal(lite.status, 0, lite.stderr);
assert.match(lite.stdout, /^mode: lite/m);
assert.match(lite.stdout, /^dry-run: yes/m);
assert.match(lite.stdout, /^file server\/index\.js$/m);
assert.match(lite.stdout, /^file tools\/setup\.mjs$/m);
assert.match(lite.stdout, /^file docs\/PLAYING\.md$/m);
assert.match(lite.stdout, /^extra public\/vendor$/m);
assert.doesNotMatch(lite.stdout, /^file tools\/golden\.mjs$/m);
assert.doesNotMatch(lite.stdout, /^file test\//m);
assert.doesNotMatch(lite.stdout, /^extra public\/assets$/m);
assert.match(lite.stdout, /^uncompressed lite: \d+/m);
assert.match(lite.stdout, /^uncompressed full estimate: \d+/m);
assert.match(lite.stdout, /puppeteer-core/);
const full = spawnSync(process.execPath, ['tools/package.mjs', '--dry-run'], {
cwd: ROOT, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
});
assert.equal(full.status, 0, full.stderr);
assert.match(full.stdout, /^mode: full/m);
assert.match(full.stdout, /^extra public\/assets$/m);
assert.match(full.stdout, /^extra public\/fonts$/m);
const assets = Number((full.stdout.match(/^assets bytes: (\d+)/m) || [])[1]);
assert.ok(assets > 300 * 1048576, `assets bytes ${assets}`);
});