Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion patches/typescript/overlay/src/compiler/tsgoChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5925,6 +5925,22 @@ function extensionFromPathOrTs(fileName: string): any {
}
return ts.Extension.Ts;
}
/**
* Issue #63: tsgo emits `{name}.d.{ext}.ts` for allowArbitraryExtensions
* source files (e.g. Button.vue → Button.vue.d.vue.ts), but the host
* convention (stock TypeScript + Volar) is `{name}.d.ts` (Button.vue.d.ts).
* Strip the redundant extra-extension segment from declaration output paths.
*/
function fixExtraExtDeclarationPath(fileName: string, extraExts: { extension: string }[] | undefined): string {
if (!extraExts?.length) return fileName;
for (const { extension } of extraExts) {
const suffix = `.d${extension}.ts`;
if (fileName.endsWith(suffix)) {
return fileName.slice(0, -suffix.length) + ".d.ts";
}
}
return fileName;
}
/**
* Builds the `extraFileExtensions` field sent with tsgo snapshots.
*
Expand Down Expand Up @@ -8501,7 +8517,9 @@ export function createTsgoProgram(
for (const o of outputs) {
// Go-computed output path — crosses the wire boundary before
// reaching writeFile/emittedFiles consumers.
const outFileName = wireFileNameToHost(o.fileName);
// Issue #63: strip the redundant extra-extension segment from
// declaration paths (e.g. Button.vue.d.vue.ts → Button.vue.d.ts).
const outFileName = fixExtraExtDeclarationPath(wireFileNameToHost(o.fileName), programCtx.pendingExtraFileExtensions);
// Builder wrappers mutate data (data.skippedDtsWrite on the
// dts-unchanged skip path in builder.ts), so a data object
// must always ride along — stock emitter threads one too.
Expand Down
3 changes: 2 additions & 1 deletion tools/ci-witness-groups.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';

const TOTAL = 72;
const TOTAL = 73;

// Witnesses intentionally NOT in the matrix — run on demand (reasons above).
const LOCAL_ONLY = [
Expand Down Expand Up @@ -158,6 +158,7 @@ const groups = [
'triage-completion-span-i55', // ~6s (npm install @types/node best-effort + tsserver session)
'triage-prototype-refresh', // ~1s
'triage-declared-type-hostonly', // ~1s
'triage-vue-tsc-decl-emit', // ~0s
],
},
{
Expand Down
84 changes: 84 additions & 0 deletions tools/triage-vue-tsc-decl-emit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* Witness for issue #63: declaration emit for `.vue` source files must produce
* `Button.vue.d.ts`, not `Button.vue.d.vue.ts`.
*
* tsgo emits `{name}.d.{ext}.ts` for allowArbitraryExtensions source files
* (e.g. Button.vue → Button.vue.d.vue.ts); the bridge must normalize this to
* `{name}.d.ts` (Button.vue.d.ts) before handing the path to the host's
* writeFile, matching stock TypeScript + Volar convention.
*
* The fixture mirrors the minimal reproduction from the issue: a .vue SFC
* with plain-TS content (Volar's virtual TS injection role) and an index.ts
* that re-exports it, compiled with declaration + emitDeclarationOnly.
*
* Exit 0 = PASS, exit 1 = FAIL.
*/
import { createRequire } from 'node:module';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

const require = createRequire(import.meta.url);
const repoRoot = path.resolve(import.meta.dirname, '..');
const ts = require(path.join(repoRoot, 'lib', 'typescript.js'));

const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tnb-vue-tsc-decl-emit-'));
fs.mkdirSync(path.join(dir, 'src', 'components'), { recursive: true });

// .vue file with plain-TS content — mirrors what Volar injects as virtual TS
// for an SFC with a <script setup lang="ts"> block.
fs.writeFileSync(path.join(dir, 'src', 'components', 'Button.vue'), 'export default {};\n');
fs.writeFileSync(path.join(dir, 'src', 'index.ts'), 'export { default as Button } from "./components/Button.vue";\n');
fs.writeFileSync(path.join(dir, 'tsconfig.json'), JSON.stringify({
compilerOptions: {
module: 'ESNext',
moduleResolution: 'Bundler',
target: 'ESNext',
strict: true,
skipLibCheck: true,
declaration: true,
emitDeclarationOnly: true,
rootDir: 'src',
outDir: 'dist',
types: [],
},
include: ['src/**/*.ts', 'src/**/*.vue'],
}));

// Register .vue as an extra script extension so collectExtraFileExtensions
// picks it up and tsgo treats the file as allowArbitraryExtensions, matching
// what vue-tsc / Volar do at program-creation time.
ts.supportedTSExtensionsFlat.push('.vue');

const configPath = path.join(dir, 'tsconfig.json');
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, {
...ts.sys,
onUnRecoverableConfigFileDiagnostic: (d) => { throw new Error(ts.flattenDiagnosticMessageText(d.messageText, '\n')); },
});
if (!parsed) {
console.error('FAIL: could not parse fixture tsconfig');
process.exit(1);
}

const program = ts.createProgram(parsed.fileNames, parsed.options);

// Capture all writeFile calls to inspect the emitted declaration paths.
const written = [];
program.emit(undefined, (fileName) => written.push(fileName));

const correct = written.filter(f => /Button\.vue\.d\.ts$/.test(f));
const broken = written.filter(f => /Button\.vue\.d\.vue\.ts$/.test(f));
const indexDts = written.some(f => /index\.d\.ts$/.test(f));

console.log('Emitted files:');
for (const f of written) console.log(' ', path.relative(dir, f));

if (broken.length || !correct.length || !indexDts) {
console.error('FAIL: wrong declaration emit for .vue source file (issue #63)');
if (broken.length) console.error(' spurious Button.vue.d.vue.ts output');
if (!correct.length) console.error(' missing Button.vue.d.ts output');
if (!indexDts) console.error(' missing index.d.ts output');
process.exit(1);
}
console.log('PASS: .vue declaration emitted as Button.vue.d.ts (not Button.vue.d.vue.ts)');
Loading