Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(typescript): emit declaration files for type-only source files that are not explicitly included #1555

Merged
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
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');
}
Comment on lines -26 to +28
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for this change is that we only need to worry about emitting .d.ts.map (and equivalent .d.cts.map & .d.mts.map) files. The .js.map files are already being emitted by the TypeScript compiler (unless the noEmit option is set to true).


/**
* 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"]
}
Loading