빌드 선언 생성 병목과 Vue 중복 타입 검사를 개선

This commit is contained in:
2026-10-23 01:33:34 +09:00
parent b377250fb9
commit af51968782
31 changed files with 436 additions and 109 deletions
@@ -0,0 +1,3 @@
import type { BuildContext } from 'tsdown';
export declare const emitDeclarations: (context: BuildContext) => Promise<void>;
+39
View File
@@ -0,0 +1,39 @@
import { spawn } from 'node:child_process';
import path from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
// JS bundling stays in tsdown. Native TypeScript emits the complete declaration
// tree instead of repeatedly bundling large inferred Prisma/tRPC type graphs.
/**
* @param {import('tsdown').BuildContext} context
* @returns {Promise<void>}
*/
export const emitDeclarations = async ({ options }) => {
const manifest = JSON.parse(await readFile(path.join(options.cwd, 'package.json'), 'utf8'));
// logic's existing wildcard export exposes dist/src/*. Keep one canonical
// declaration tree so private class members retain their identity across
// root and subpath imports, and keep the wildcard JS entry points working.
const sourceExports = manifest.exports?.['./*'] === './dist/src/*';
const args = [
'-w',
'tsc7',
'-p',
path.join(options.cwd, 'tsconfig.build.json'),
...(sourceExports ? [] : ['--emitDeclarationOnly']),
'--types',
'node',
'--outDir',
sourceExports ? path.join(options.outDir, 'src') : options.outDir,
];
await new Promise((resolve, reject) => {
const child = spawn('pnpm', args, { cwd: options.cwd, stdio: 'inherit' });
child.once('error', reject);
child.once('close', (code, signal) => {
if (code === 0) resolve(undefined);
else reject(new Error(`Declaration emission failed (${signal ?? code ?? 'unknown'}): ${options.cwd}`));
});
});
if (sourceExports) {
await writeFile(path.join(options.outDir, 'index.d.ts'), "export * from './src/index.js';\n");
}
};
@@ -0,0 +1,73 @@
import assert from 'node:assert/strict';
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import { execFileSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';
import { emitDeclarations } from './emit-declarations.mjs';
const workspaceRoot = path.resolve(import.meta.dirname, '../..');
test('native declarations preserve relative imports, subpaths and maps, and fail on type errors', async () => {
const temporaryRoot = path.join(workspaceRoot, '.turbo');
await mkdir(temporaryRoot, { recursive: true });
const fixture = await mkdtemp(path.join(temporaryRoot, 'declaration-test-'));
const outDir = path.join(fixture, 'dist');
try {
await mkdir(path.join(fixture, 'src'));
await writeFile(path.join(fixture, 'package.json'), JSON.stringify({ type: 'module' }));
await writeFile(
path.join(fixture, 'tsconfig.build.json'),
JSON.stringify({
extends: path.join(workspaceRoot, 'tsconfig.base.json'),
compilerOptions: { rootDir: 'src', outDir: 'dist' },
include: ['src'],
})
);
await writeFile(
path.join(fixture, 'src/model.ts'),
'export class Model { private readonly id = 1; getId(): number { return this.id; } }\n'
);
await writeFile(path.join(fixture, 'src/index.ts'), "export { Model } from './model.js';\n");
await emitDeclarations({ options: { cwd: fixture, outDir } });
assert.match(await readFile(path.join(outDir, 'index.d.ts'), 'utf8'), /from ['"]\.\/model\.js['"]/u);
assert.match(await readFile(path.join(outDir, 'model.d.ts'), 'utf8'), /getId\(\): number/u);
const map = JSON.parse(await readFile(path.join(outDir, 'model.d.ts.map'), 'utf8'));
assert.deepEqual(map.sources, ['../src/model.ts']);
await assert.rejects(readFile(path.join(outDir, 'index.js')), { code: 'ENOENT' });
await writeFile(
path.join(fixture, 'package.json'),
JSON.stringify({ type: 'module', exports: { './*': './dist/src/*' } })
);
await emitDeclarations({ options: { cwd: fixture, outDir } });
assert.equal(await readFile(path.join(outDir, 'index.d.ts'), 'utf8'), "export * from './src/index.js';\n");
const { Model } = await import(pathToFileURL(path.join(outDir, 'src/model.js')).href);
assert.equal(new Model().getId(), 1);
await readFile(path.join(outDir, 'src/model.d.ts.map'));
await readFile(path.join(outDir, 'src/model.js.map'));
await writeFile(
path.join(fixture, 'consumer.ts'),
"import { Model } from './dist/index.js';\n" +
"import { Model as SubpathModel } from './dist/src/model.js';\n" +
'export const same: Model = new SubpathModel();\n'
);
await writeFile(
path.join(fixture, 'tsconfig.consumer.json'),
JSON.stringify({ extends: path.join(workspaceRoot, 'tsconfig.base.json'), files: ['consumer.ts'] })
);
execFileSync(
'pnpm',
['-w', 'tsc7', '-p', path.join(fixture, 'tsconfig.consumer.json'), '--noEmit', '--types', 'node'],
{
cwd: fixture,
stdio: 'pipe',
}
);
await writeFile(path.join(fixture, 'src/model.ts'), 'export const id: number = "invalid";\n');
await assert.rejects(emitDeclarations({ options: { cwd: fixture, outDir } }), /Declaration emission failed/u);
} finally {
await rm(fixture, { recursive: true, force: true });
}
});
+1 -1
View File
@@ -7,7 +7,7 @@
"build": "pnpm -w tsc7 -p tools/build-scripts/tsconfig.json",
"dev": "node -e \"console.log('dev not configured')\"",
"lint": "node -e \"console.log('lint not configured')\"",
"test": "node -e \"console.log('test not configured')\"",
"test": "node --test *.test.mjs",
"typecheck": "pnpm -w tsc7 -b tools/build-scripts/tsconfig.json",
"build:server": "node ./build-server.mjs"
}