Merge branch 'phase/57-client-typecheck'

# Conflicts:
#	docs/phases/10-craft/README.md
This commit is contained in:
Leonid Pershin
2026-08-20 13:12:16 +03:00
8 changed files with 137 additions and 25 deletions
-7
View File
@@ -7,9 +7,6 @@
"": {
"name": "hschool-client",
"version": "0.1.0",
"dependencies": {
"hschool-client": "file:"
},
"devDependencies": {
"@types/node": "^24.10.1",
"happy-dom": "^20.11.2",
@@ -597,10 +594,6 @@
"node": ">=20.0.0"
}
},
"node_modules/hschool-client": {
"resolved": "",
"link": true
},
"node_modules/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+1 -4
View File
@@ -11,7 +11,7 @@
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc -b",
"test": "vitest run",
"test": "node ./scripts/run-test.mjs",
"test:watch": "vitest"
},
"devDependencies": {
@@ -20,8 +20,5 @@
"typescript": "~5.9.3",
"vite": "^8.2.1",
"vitest": "^4.1.10"
},
"dependencies": {
"hschool-client": "file:"
}
}
@@ -0,0 +1,81 @@
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
const clientRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(import.meta.url);
const tscBin = require.resolve('typescript/bin/tsc');
const pkg = JSON.parse(readFileSync(path.join(clientRoot, 'package.json'), 'utf8')) as {
scripts: Record<string, string>;
dependencies?: Record<string, string>;
};
describe('npm test script', () => {
it('runs typecheck then vitest, without swallowing tsc', () => {
expect(pkg.scripts.test).toBe('node ./scripts/run-test.mjs');
const runner = readFileSync(path.join(clientRoot, 'scripts', 'run-test.mjs'), 'utf8');
expect(runner).toContain("'-b'");
expect(runner).toContain("'run'");
expect(runner).toMatch(/if \(typecheck !== 0\)/);
expect(pkg.scripts.typecheck).toBe('tsc -b');
});
it('keeps watch as vitest only', () => {
expect(pkg.scripts['test:watch']).toBe('vitest');
expect(pkg.scripts['test:watch']).not.toMatch(/typecheck|tsc/);
});
it('does not depend on the hschool-client file: loop', () => {
expect(pkg.dependencies?.['hschool-client']).toBeUndefined();
});
});
describe('duplicate dictionary keys', () => {
it('makes tsc fail on a strings.ts ru object without vite build', () => {
const dir = mkdtempSync(path.join(tmpdir(), 'hschool-dup-'));
try {
const original = readFileSync(path.join(clientRoot, 'src', 'i18n', 'strings.ts'), 'utf8');
const needle = " createSchool: 'Создать школу',";
expect(original).toContain(needle);
const duplicate = original.replace(
needle,
`${needle}\n createSchool: 'дубликат',`,
);
writeFileSync(path.join(dir, 'strings.ts'), duplicate);
writeFileSync(
path.join(dir, 'locale.ts'),
readFileSync(path.join(clientRoot, 'src', 'i18n', 'locale.ts'), 'utf8'),
);
writeFileSync(
path.join(dir, 'tsconfig.json'),
JSON.stringify({
compilerOptions: {
strict: true,
noEmit: true,
target: 'ES2022',
module: 'ESNext',
moduleResolution: 'bundler',
allowImportingTsExtensions: true,
verbatimModuleSyntax: true,
lib: ['ES2022', 'DOM'],
},
include: ['strings.ts', 'locale.ts'],
}),
);
const result = spawnSync(process.execPath, [tscBin, '-p', dir], {
encoding: 'utf8',
});
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`;
expect(result.status).not.toBe(0);
expect(output).toMatch(/TS1117|TS2300|TS1118/);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});
+38
View File
@@ -0,0 +1,38 @@
// `tsc -b && vitest run` on Windows npm (tsc.cmd) can drop the typecheck exit code.
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const clientRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(import.meta.url);
const win = process.platform === 'win32';
function statusOf(result) {
if (result.error) {
console.error(result.error);
return 1;
}
return result.status ?? 1;
}
const typecheck = statusOf(
spawnSync(process.execPath, [require.resolve('typescript/bin/tsc'), '-b'], {
cwd: clientRoot,
stdio: 'inherit',
}),
);
if (typecheck !== 0) {
process.exit(typecheck);
}
const vitest = path.join(clientRoot, 'node_modules', '.bin', win ? 'vitest.cmd' : 'vitest');
process.exit(
statusOf(
spawnSync(vitest, ['run', ...process.argv.slice(2)], {
cwd: clientRoot,
stdio: 'inherit',
shell: win,
}),
),
);
+1 -1
View File
@@ -20,5 +20,5 @@
"noUncheckedSideEffectImports": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
"include": ["vite.config.ts", "scripts/**/*.ts"]
}