Skip to content

Commit

Permalink
fix(typescript): emit declaration files for type-only source files th…
Browse files Browse the repository at this point in the history
…at are not explicitly included (#1555)

* test(typescript): add test case for implicitly included declarations

* fix(typescript): emit declarations for (implicit) type-only source files

* test(typescript): update declaration tests output file order

* test(typescript): verify that only necessary declarations are emitted
  • Loading branch information
ianyong committed Aug 26, 2023
1 parent 303ff16 commit 027bca6
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 45 deletions.
77 changes: 42 additions & 35 deletions packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import createModuleResolver from './moduleResolution';
import { getPluginOptions } from './options/plugin';
import { emitParsedOptionsErrors, parseTypescriptConfig } from './options/tsconfig';
import { validatePaths, validateSourceMap } from './options/validate';
import findTypescriptOutput, { getEmittedFile, normalizePath, emitFile } from './outputFile';
import findTypescriptOutput, {
getEmittedFile,
normalizePath,
emitFile,
isDeclarationOutputFile,
isMapOutputFile
} from './outputFile';
import { preflight } from './preflight';
import createWatchProgram, { WatchProgramHelper } from './watchProgram';
import TSCache from './tscache';
Expand Down Expand Up @@ -150,40 +156,41 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
},

async generateBundle(outputOptions) {
parsedOptions.fileNames.forEach((fileName) => {
const output = findTypescriptOutput(ts, parsedOptions, fileName, emittedFiles, tsCache);
output.declarations.forEach((id) => {
const code = getEmittedFile(id, emittedFiles, tsCache);
if (!code || !parsedOptions.options.declaration) {
return;
}

let baseDir: string | undefined;
if (outputOptions.dir) {
baseDir = outputOptions.dir;
} else if (outputOptions.file) {
// find common path of output.file and configured declation output
const outputDir = path.dirname(outputOptions.file);
const configured = path.resolve(
parsedOptions.options.declarationDir ||
parsedOptions.options.outDir ||
tsconfig ||
process.cwd()
);
const backwards = path
.relative(outputDir, configured)
.split(path.sep)
.filter((v) => v === '..')
.join(path.sep);
baseDir = path.normalize(`${outputDir}/${backwards}`);
}
if (!baseDir) return;

this.emitFile({
type: 'asset',
fileName: normalizePath(path.relative(baseDir, id)),
source: code
});
const declarationAndMapFiles = [...emittedFiles.keys()].filter(
(fileName) => isDeclarationOutputFile(fileName) || isMapOutputFile(fileName)
);

declarationAndMapFiles.forEach((id) => {
const code = getEmittedFile(id, emittedFiles, tsCache);
if (!code || !parsedOptions.options.declaration) {
return;
}

let baseDir: string | undefined;
if (outputOptions.dir) {
baseDir = outputOptions.dir;
} else if (outputOptions.file) {
// find common path of output.file and configured declation output
const outputDir = path.dirname(outputOptions.file);
const configured = path.resolve(
parsedOptions.options.declarationDir ||
parsedOptions.options.outDir ||
tsconfig ||
process.cwd()
);
const backwards = path
.relative(outputDir, configured)
.split(path.sep)
.filter((v) => v === '..')
.join(path.sep);
baseDir = path.normalize(`${outputDir}/${backwards}`);
}
if (!baseDir) return;

this.emitFile({
type: 'asset',
fileName: normalizePath(path.relative(baseDir, id)),
source: code
});
});

Expand Down
15 changes: 11 additions & 4 deletions packages/typescript/src/outputFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,22 @@ export interface TypescriptSourceDescription extends Partial<SourceDescription>
/**
* Checks if the given OutputFile represents some code
*/
function isCodeOutputFile(name: string): boolean {
return !isMapOutputFile(name) && !name.endsWith('.d.ts');
export function isCodeOutputFile(name: string): boolean {
return !isMapOutputFile(name) && !isDeclarationOutputFile(name);
}

/**
* Checks if the given OutputFile represents some source map
*/
function isMapOutputFile(name: string): boolean {
return name.endsWith('.map');
export function isMapOutputFile(name: string): boolean {
return name.endsWith('ts.map');
}

/**
* Checks if the given OutputFile represents some declaration
*/
export function isDeclarationOutputFile(name: string): boolean {
return /\.d\.[cm]?ts$/.test(name);
}

/**
Expand Down
43 changes: 37 additions & 6 deletions packages/typescript/test/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ test.serial('supports creating declaration files in subfolder', async (t) => {
onwarn
});
const output = await getCode(bundle, { format: 'es', dir: 'fixtures/basic/dist' }, true);
const declaration = output[1].source as string;
const declaration = output[2].source as string;

t.deepEqual(
output.map((out) => out.fileName),
['main.js', 'types/main.d.ts', 'types/main.d.ts.map']
['main.js', 'types/main.d.ts.map', 'types/main.d.ts']
);

t.true(declaration.includes('declare const answer = 42;'), declaration);
Expand Down Expand Up @@ -100,23 +100,54 @@ test.serial('supports creating declaration files for interface only source file'
{ format: 'es', dir: 'fixtures/export-interface-only/dist' },
true
);
const declaration = output[1].source as string;
const declaration = output[2].source as string;

t.deepEqual(
output.map((out) => out.fileName),
[
'main.js',
'types/interface.d.ts',
'types/interface.d.ts.map',
'types/main.d.ts',
'types/main.d.ts.map'
'types/interface.d.ts',
'types/main.d.ts.map',
'types/main.d.ts'
]
);

t.true(declaration.includes('export interface ITest'), declaration);
t.true(declaration.includes('//# sourceMappingURL=interface.d.ts.map'), declaration);
});

test.serial(
'supports creating declaration files for type-only source files that are implicitly included',
async (t) => {
const bundle = await rollup({
input: 'fixtures/implicitly-included-type-only-file/main.ts',
plugins: [
typescript({
tsconfig: 'fixtures/implicitly-included-type-only-file/tsconfig.json',
declarationDir: 'fixtures/implicitly-included-type-only-file/dist/types',
declaration: true
}),
onwarn
]
});
const output = await getCode(
bundle,
{ format: 'es', dir: 'fixtures/implicitly-included-type-only-file/dist' },
true
);
const declaration = output[1].source as string;

t.deepEqual(
output.map((out) => out.fileName),
// 'types/should-not-be-emitted-types.d.ts' should not be emitted because 'main.ts' does not import/export from it.
['main.js', 'types/should-be-emitted-types.d.ts', 'types/main.d.ts']
);

t.true(declaration.includes('export declare type MyNumber = number;'), declaration);
}
);

test.serial('supports creating declaration files in declarationDir', async (t) => {
const bundle = await rollup({
input: 'fixtures/basic/main.ts',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MyNumber } from './should-be-emitted-types';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type MyNumber = number;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is intentionally not imported from.
export type MyString = string;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"include": ["main.ts"]
}

0 comments on commit 027bca6

Please sign in to comment.