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) replace getOutputFileNames #726

Closed
wants to merge 1 commit into from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"dependencies": {
"@rollup/pluginutils": "^3.1.0",
"common-path-prefix": "^3.0.0",
"resolve": "^1.17.0"
},
"devDependencies": {
Expand Down
69 changes: 55 additions & 14 deletions packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as path from 'path';

import { Plugin, SourceDescription } from 'rollup';
import type { Watch } from 'typescript';
import { NormalizedOutputOptions, Plugin, SourceDescription } from 'rollup';
import type { SourceFile, Watch } from 'typescript';
import CommonPathPrefix from 'common-path-prefix';

import { RollupTypescriptOptions } from '../types';

Expand All @@ -10,7 +11,7 @@ import createModuleResolver from './moduleResolution';
import getPluginOptions from './options/plugin';
import { emitParsedOptionsErrors, parseTypescriptConfig } from './options/tsconfig';
import { validatePaths, validateSourceMap } from './options/validate';
import findTypescriptOutput, { getEmittedFile } from './outputFile';
import getTSGeneratedOutput, { getEmittedFile, TSGeneratedFile } from './outputFile';
import createWatchProgram, { WatchProgramHelper } from './watchProgram';
import TSCache from './tscache';

Expand All @@ -25,7 +26,9 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
typescript: ts
} = getPluginOptions(options);
const tsCache = new TSCache(cacheDir);
const emittedFiles = new Map<string, string>();

const tsGeneratedFiles = new Map<string, TSGeneratedFile>();
let commonPathPrefix = ``;
const watchProgramHelper = new WatchProgramHelper();

const parsedOptions = parseTypescriptConfig(ts, tsconfig, compilerOptions);
Expand All @@ -52,11 +55,24 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
formatHost,
resolveModule,
parsedOptions,
writeFile(fileName, data) {
writeFile(
fileName: string,
data: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_writeByteOrderMark,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_onError?,
sourceFiles?: readonly SourceFile[]
) {
if (parsedOptions.options.composite || parsedOptions.options.incremental) {
tsCache.cacheCode(fileName, data);
}
emittedFiles.set(fileName, data);
tsGeneratedFiles.set(fileName, {
data,
sourceFileNames: sourceFiles?.map<string>(
(sourceFile: SourceFile) => sourceFile.fileName
)
});
},
status(diagnostic) {
watchProgramHelper.handleStatus(diagnostic);
Expand All @@ -78,6 +94,13 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
// eslint-disable-next-line
program?.close();
}
// Calculate the common path prefix of all input & output file.
// We will use this as a fallback for the "baseDir" for outputting declarations in generateBundle.
const inputFilenames = [...parsedOptions.fileNames];
tsGeneratedFiles.forEach((_tsGeneratedFile: TSGeneratedFile, filename: string) =>
inputFilenames.push(filename)
);
commonPathPrefix = CommonPathPrefix(inputFilenames);
},

renderStart(outputOptions) {
Expand Down Expand Up @@ -116,21 +139,39 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
parsedOptions.fileNames.push(fileName);
}

const output = findTypescriptOutput(ts, parsedOptions, id, emittedFiles, tsCache);
const output = getTSGeneratedOutput(id, tsGeneratedFiles, tsCache);

return output.code != null ? (output as SourceDescription) : null;
},

generateBundle(outputOptions) {
generateBundle(outputOptions: NormalizedOutputOptions) {
parsedOptions.fileNames.forEach((fileName) => {
const output = findTypescriptOutput(ts, parsedOptions, fileName, emittedFiles, tsCache);
const output = getTSGeneratedOutput(fileName, tsGeneratedFiles, tsCache);
output.declarations.forEach((id) => {
const code = getEmittedFile(id, emittedFiles, tsCache);
const code = getEmittedFile(id, tsGeneratedFiles, tsCache);
let baseDir = outputOptions.dir;
if (!baseDir && tsconfig) {
baseDir = tsconfig.substring(0, tsconfig.lastIndexOf('/'));

// Use the same logic as typescript to decide where to put the declarations.
// https://www.typescriptlang.org/tsconfig#declarationDir
// https://www.typescriptlang.org/tsconfig#outDir
// Using the longest common path prefix as a fallback.
if (!baseDir) {
baseDir = parsedOptions.options.declarationDir
? parsedOptions.options.declarationDir
: parsedOptions.options.outDir
? parsedOptions.options.outDir
: commonPathPrefix;
}

if (!baseDir) {
this.error(
`@rollup/plugin-typescript: Unable to determine directory to write typescript declaration files. Please specify 'declarationDir' or 'outDir' in your compiler options.`
);
}

if (!code) {
return;
}
if (!code || !baseDir) return;

this.emitFile({
type: 'asset',
Expand All @@ -145,7 +186,7 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
this.emitFile({
type: 'asset',
fileName: normalizePath(path.relative(outputOptions.dir!, tsBuildInfoPath)),
source: emittedFiles.get(tsBuildInfoPath)
source: tsGeneratedFiles.get(tsBuildInfoPath)?.data
});
}
}
Expand Down
51 changes: 31 additions & 20 deletions packages/typescript/src/outputFile.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { SourceDescription } from 'rollup';
import type { ParsedCommandLine } from 'typescript';

import TSCache from './tscache';

export interface TypescriptSourceDescription extends Partial<SourceDescription> {
declarations: string[];
}

export interface TSGeneratedFile {
data: string;
sourceFileNames?: string[];
}
/**
* Checks if the given OutputFile represents some code
*/
Expand All @@ -30,13 +32,13 @@ function isMapOutputFile(name: string): boolean {
*/
export function getEmittedFile(
fileName: string | undefined,
emittedFiles: ReadonlyMap<string, string>,
emittedFiles: ReadonlyMap<string, TSGeneratedFile>,
tsCache: TSCache
): string | undefined {
let code: string | undefined;
if (fileName) {
if (emittedFiles.has(fileName)) {
code = emittedFiles.get(fileName);
code = emittedFiles.get(fileName)?.data;
} else {
code = tsCache.getCached(fileName);
}
Expand All @@ -46,29 +48,38 @@ export function getEmittedFile(

/**
* Finds the corresponding emitted Javascript files for a given Typescript file.
* @param id Path to the Typescript file.
* @param source Path to the Typescript file.
* @param emittedFiles Map of file names to source code,
* containing files emitted by the Typescript compiler.
*/
export default function findTypescriptOutput(
ts: typeof import('typescript'),
parsedOptions: ParsedCommandLine,
id: string,
emittedFiles: ReadonlyMap<string, string>,
export default function getTSGeneratedOutput(
sourceFilename: string,
tsGeneratedFiles: ReadonlyMap<string, TSGeneratedFile>,
tsCache: TSCache
): TypescriptSourceDescription {
const emittedFileNames = ts.getOutputFileNames(
parsedOptions,
id,
!ts.sys.useCaseSensitiveFileNames
);
// We only want to consider tsGeneratedFiles that have the source listed as one of their inputs.
const tsGeneratedFileNames: string[] = [];
tsGeneratedFiles.forEach((tsGeneratedFile: TSGeneratedFile, filename: string) => {
if (tsGeneratedFile.sourceFileNames?.includes(sourceFilename)) {
tsGeneratedFileNames.push(filename);
}
});

const codeFile = emittedFileNames.find(isCodeOutputFile);
const mapFile = emittedFileNames.find(isMapOutputFile);
const codeFilename = tsGeneratedFileNames.find(isCodeOutputFile);
const mapFilename = tsGeneratedFileNames.find(
(candidateMapFilename) =>
codeFilename &&
isMapOutputFile(candidateMapFilename) &&
candidateMapFilename.includes(codeFilename)
);

return {
code: getEmittedFile(codeFile, emittedFiles, tsCache),
map: getEmittedFile(mapFile, emittedFiles, tsCache),
declarations: emittedFileNames.filter((name) => name !== codeFile && name !== mapFile)
code: getEmittedFile(codeFilename, tsGeneratedFiles, tsCache),
map: getEmittedFile(mapFilename, tsGeneratedFiles, tsCache),
declarations: tsGeneratedFileNames.filter(
(candidateDeclarationFilename) =>
candidateDeclarationFilename !== codeFilename &&
candidateDeclarationFilename !== mapFilename
)
};
}
49 changes: 38 additions & 11 deletions packages/typescript/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ test.serial('supports creating declaration files in subfolder', async (t) => {

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']
);

const declarationSource = output[1].source;
const declarationSource = output[2].source;
t.true(declarationSource.includes('declare const answer = 42;'), declarationSource);
t.true(declarationSource.includes('//# sourceMappingURL=main.d.ts.map'), declarationSource);
});
Expand Down Expand Up @@ -126,17 +126,17 @@ test.serial('supports creating declaration files for interface only source file'
);

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

const declarationSource = output[1].source;
const declarationSource = output[2].source;
t.true(declarationSource.includes('export interface ITest'), declarationSource);
t.true(declarationSource.includes('//# sourceMappingURL=interface.d.ts.map'), declarationSource);
});
Expand All @@ -155,6 +155,30 @@ test.serial('supports creating declaration files in declarationDir', async (t) =
});
const output = await getCode(bundle, { format: 'esm', dir: 'fixtures/basic/dist' }, true);

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

t.true(output[1].source.includes('declare const answer = 42;'), output[1].source);
});

test.serial('Should support creating declaration files when not using tsconfig.json', async (t) => {
const bundle = await rollup({
input: 'fixtures/basic/main.ts',
plugins: [
typescript({
include: 'fixtures/basic/*.ts',
exclude: 'fixtures/basic/dist/types',
tsconfig: false,
declaration: true,
declarationDir: 'fixtures/basic/dist/types'
})
],
onwarn
});
const output = await getCode(bundle, { format: 'esm', dir: 'fixtures/basic/dist' }, true);

t.deepEqual(
output.map((out) => out.fileName),
['main.js', 'types/main.d.ts']
Expand Down Expand Up @@ -260,7 +284,7 @@ test.serial('ensures multiple outputs can be built', async (t) => {

t.deepEqual(
[...new Set(output1.concat(output2).map((out) => out.fileName))].sort(),
['index.d.ts', 'index.js', 'server.d.ts', 'server.js']
['index.d.ts', 'index.js', 'server.d.ts', 'server.js'].sort()
);
});

Expand Down Expand Up @@ -461,7 +485,10 @@ test.serial('supports overriding the TypeScript version', async (t) => {
emit(_, writeFile) {
writeFile(
path.join(__dirname, 'fixtures/overriding-typescript/main.js'),
'export default 1337;'
'export default 1337;',
false,
undefined,
[{ fileName: path.join(__dirname, 'fixtures/overriding-typescript/main.ts') }]
);
}
};
Expand All @@ -470,7 +497,7 @@ test.serial('supports overriding the TypeScript version', async (t) => {
// Call the overrided emit function to trigger writeFile
program.emit();

return { close() {} };
return { close() { } };
}
})
})
Expand Down Expand Up @@ -1081,8 +1108,8 @@ test('supports custom transformers', async (t) => {
// Expect a TypeChecker to have been forwarded for transformers with custom factories requesting one
t.deepEqual(
typeChecker &&
typeChecker.getTypeAtLocation &&
typeof typeChecker.getTypeAtLocation === 'function',
typeChecker.getTypeAtLocation &&
typeof typeChecker.getTypeAtLocation === 'function',
true
);
});
Expand All @@ -1104,7 +1131,7 @@ function fakeTypescript(custom) {

createWatchCompilerHost() {
return {
afterProgramCreate() {}
afterProgramCreate() { }
};
},

Expand Down
3 changes: 2 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.