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:
commit
3288afc787
14 files changed
+2174
-7
No files matched your search
@@ -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/);
|
||||
});
|
||||
@@ -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}`);
|
||||
});
|
||||
Reference in new issue
Block a user