빌드 선언 생성 병목과 Vue 중복 타입 검사를 개선
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import type { BuildContext } from 'tsdown';
|
||||
|
||||
export declare const emitDeclarations: (context: BuildContext) => Promise<void>;
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
||||
const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const expectedApiVersion = '6.0.3';
|
||||
const expectedCliVersion = '7.0.2';
|
||||
const expectedVueBridgeVersion = '6.0.3-bridge.18.tsgo.7.0.2';
|
||||
|
||||
const consumers = [
|
||||
['root', 'package.json'],
|
||||
@@ -35,4 +36,10 @@ if (cliVersion !== expectedCliVersion) {
|
||||
throw new Error(`native tsc resolved ${cliVersion}; expected ${expectedCliVersion}`);
|
||||
}
|
||||
|
||||
console.log(`TypeScript API ${expectedApiVersion}; native tsc ${expectedCliVersion}`);
|
||||
const requireFromRoot = createRequire(path.join(workspaceRoot, 'package.json'));
|
||||
const vueBridgeVersion = requireFromRoot('typescript-native-bridge/package.json').version;
|
||||
if (vueBridgeVersion !== expectedVueBridgeVersion) {
|
||||
throw new Error(`Vue bridge resolved ${vueBridgeVersion}; expected ${expectedVueBridgeVersion}`);
|
||||
}
|
||||
|
||||
console.log(`TypeScript API ${expectedApiVersion}; native tsc ${expectedCliVersion}; Vue bridge ${vueBridgeVersion}`);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
|
||||
// Scope the bridge to the Vue CLI. Vite, ESLint and other compiler API
|
||||
// consumers keep resolving the workspace's stock TypeScript 6 installation.
|
||||
const requireFromFrontend = createRequire(path.resolve('package.json'));
|
||||
const requireFromToolchain = createRequire(import.meta.url);
|
||||
const { run } = requireFromFrontend('vue-tsc');
|
||||
run(requireFromToolchain.resolve('typescript-native-bridge/lib/tsc.js'));
|
||||
Reference in New Issue
Block a user