Resolve dist imports in runtime scripts against their TypeScript sources. Keep missing-module checks enabled and cover absent build output in the lint gate. Verified npm ci, prisma generate and full verify with dist absent at startup.
117 lines
6.6 KiB
JavaScript
117 lines
6.6 KiB
JavaScript
const { ESLint } = require('eslint');
|
|
const assert = require('node:assert/strict');
|
|
const { mkdtempSync, rmSync, writeFileSync } = require('node:fs');
|
|
const { basename, join } = require('node:path');
|
|
const { test } = require('node:test');
|
|
|
|
const eslint = new ESLint();
|
|
|
|
test('runtime scripts resolve build imports against sources before dist exists', async () => {
|
|
const directory = mkdtempSync(join(process.cwd(), '.eslint-require-'));
|
|
try {
|
|
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Test-owned temporary source with no compiled counterpart.
|
|
writeFileSync(join(directory, 'entry.ts'), 'export const value = true;');
|
|
for (const [filePath, prefix] of [
|
|
['scripts/generate-openapi.cjs', '../'],
|
|
['test/runtime-check.cjs', '../'],
|
|
['test/fixtures/tbank-sync-crash.cjs', '../../'],
|
|
]) {
|
|
const source = `${prefix}dist/${basename(directory)}/entry`;
|
|
const [result] = await eslint.lintText(
|
|
`require('${source}.js'); require('${source}'); require('node:fs'); require('eslint'); require('${source}-missing.js'); require('${prefix}missing-local.cjs'); require('wealthledger-missing-package');`,
|
|
{ filePath },
|
|
);
|
|
assert.equal(result.fatalErrorCount, 0, filePath);
|
|
const missing = result.messages.filter(({ ruleId }) => ruleId === 'n/no-missing-require');
|
|
assert.equal(missing.length, 3, filePath);
|
|
for (const target of [`${source}-missing.js`, `${prefix}missing-local.cjs`, 'wealthledger-missing-package']) {
|
|
assert.ok(missing.some(({ message }) => message.includes(`'${target}'`)), target);
|
|
}
|
|
}
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// Protect the quality gate's file scopes when configs or plugin versions change.
|
|
test('typed application code enforces async and unsafe-type checks', async () => {
|
|
const [result] = await eslint.lintText(
|
|
'export const unsafe: any = 1; Promise.resolve(unsafe);',
|
|
{ filePath: 'src/main.ts' },
|
|
);
|
|
const rules = new Set(result.messages.map(({ ruleId }) => ruleId));
|
|
assert.ok(rules.has('@typescript-eslint/no-explicit-any'));
|
|
assert.ok(rules.has('@typescript-eslint/no-floating-promises'));
|
|
});
|
|
|
|
test('framework and runtime rules reach their intended files', async () => {
|
|
const app = await eslint.calculateConfigForFile('src/main.ts');
|
|
const spec = await eslint.calculateConfigForFile('test/app.e2e-spec.ts');
|
|
const script = await eslint.calculateConfigForFile('scripts/generate-openapi.cjs');
|
|
const config = await eslint.calculateConfigForFile('eslint.config.mjs');
|
|
assert.equal(app.rules['@darraghor/nestjs-typed/validation-pipe-should-use-forbid-unknown'][0], 2);
|
|
assert.equal(spec.rules['jest/no-focused-tests'][0], 2);
|
|
assert.equal(app.rules['jest/no-focused-tests'], undefined);
|
|
assert.equal(app.languageOptions.globals.expect, undefined);
|
|
assert.equal(config.languageOptions.globals.require, 'off');
|
|
for (const profile of [app, spec, script, config]) {
|
|
assert.equal(profile.rules['n/no-deprecated-api'][0], 2);
|
|
assert.equal(profile.rules['security/detect-eval-with-expression'][0], 1);
|
|
assert.equal(profile.rules['regexp/no-super-linear-backtracking'][0], 2);
|
|
assert.equal(profile.rules['sonarjs/cognitive-complexity'][0], 2);
|
|
assert.equal(profile.rules['import-x/no-unresolved'][0], 2);
|
|
assert.equal(profile.rules['perfectionist/sort-imports'][0], 2);
|
|
assert.equal(profile.rules['@stylistic/semi'][0], 2);
|
|
}
|
|
});
|
|
|
|
test('focused tests are rejected and generated code is excluded', async () => {
|
|
const [result] = await eslint.lintText(
|
|
'it.only(\'example\', () => { expect(1).toBe(1); });',
|
|
{ filePath: 'test/app.e2e-spec.ts' },
|
|
);
|
|
assert.ok(result.messages.some(({ ruleId }) => ruleId === 'jest/no-focused-tests'));
|
|
assert.ok(await eslint.isPathIgnored('generated/prisma/client.ts'));
|
|
assert.ok(await eslint.isPathIgnored('dist/src/main.js'));
|
|
});
|
|
|
|
test('numeric literals require names, including previously ignored numbers and bigints', async () => {
|
|
const literals = [-60, -24, -10, -2, -1, 0, 1, 2, 10, 24, 60].map(String);
|
|
for (const literal of [...literals, ...literals.map((value) => `${value}n`)]) {
|
|
const [inline] = await eslint.lintText(
|
|
`export function value() { return ${literal}; }`,
|
|
{ filePath: 'src/main.ts' },
|
|
);
|
|
assert.ok(inline.messages.some(({ ruleId }) => ruleId === '@typescript-eslint/no-magic-numbers'), literal);
|
|
const [named] = await eslint.lintText(
|
|
`const boundary = ${literal}; export function value() { return boundary; }`,
|
|
{ filePath: 'src/main.ts' },
|
|
);
|
|
assert.ok(!named.messages.some(({ ruleId }) => ruleId === '@typescript-eslint/no-magic-numbers'), literal);
|
|
}
|
|
});
|
|
|
|
// Single-use codes must be caught too; fixture literals remain independent wire assertions.
|
|
test('machine codes require enum members without banning ordinary text or Prisma selects', async () => {
|
|
const eslint = new ESLint();
|
|
const filePath = 'src/main.ts';
|
|
for (const code of ['export function check(code: string) { return code === "one-off-code"; }', 'export const result = `MOEX_UNAVAILABLE`;', 'export const result = { code: "one-off-code" };', 'export const result = { "errorCode": "one-off-code" };', 'export const result = { code: `dynamic-${Date.now()}` };', 'export const result = "MOEX_UNAVAILABLE";', 'export const result = ["INSTRUMENT_ID_TYPE_FIGI"];', 'export const result = { OPERATION_TYPE_COUPON: "income" };']) {
|
|
const [result] = await eslint.lintText(code, { filePath });
|
|
assert.equal(result.fatalErrorCount, 0, code);
|
|
assert.ok(result.messages.some(({ ruleId }) => ruleId === 'no-restricted-syntax'), code);
|
|
}
|
|
for (const code of [
|
|
'enum Code { Unavailable = "MOEX_UNAVAILABLE" } export const result = { code: Code.Unavailable };',
|
|
'export const select = { code: true, errorCode: true };',
|
|
'export const label = "Please try again"; export const key = "JWT_SECRET";',
|
|
]) {
|
|
const [result] = await eslint.lintText(code, { filePath });
|
|
assert.equal(result.fatalErrorCount, 0, code);
|
|
assert.ok(!result.messages.some(({ ruleId }) => ruleId === 'no-restricted-syntax'), code);
|
|
}
|
|
const [fixture] = await eslint.lintText('export const result = { code: "MOEX_UNAVAILABLE" };', { filePath: 'src/integrations/moex/moex.client.spec.ts' });
|
|
assert.ok(!fixture.messages.some(({ ruleId }) => ruleId === 'no-restricted-syntax'));
|
|
const [comparison] = await eslint.lintText('enum Code { Ready = "ready" } export function check(code: Code) { return code === "ready"; }', { filePath });
|
|
assert.ok(comparison.messages.some(({ ruleId }) => ruleId === '@typescript-eslint/no-unsafe-enum-comparison'));
|
|
});
|