Skip to content

Commit 96b0338

Browse files
authored
feat(typescript): write declaration files in configured directory for output.file (#1378)
* fix(typescript): Add missing types for `resolve` Signed-off-by: Ferdinand Thiessen <[email protected]> * feat(utils): Add test function `getFiles` This function returns the file names and content as rollup would write to filesystem. Emulating `handleGenerateWrite` and `writeOutputFile`. Signed-off-by: Ferdinand Thiessen <[email protected]> * fix(typescript): Fix writing declarations when `output.file` is used This fixes writing declarations into the configured `declarationDir` when configured rollup build with `output.file` instead of `output.dir`. Signed-off-by: Ferdinand Thiessen <[email protected]> * docs(typescript): Remove now unneeded documentation of non functional workaround Signed-off-by: Ferdinand Thiessen <[email protected]> * fix: Make `generateBundle` function more self explaining Signed-off-by: Ferdinand Thiessen <[email protected]> --------- Signed-off-by: Ferdinand Thiessen <[email protected]>
1 parent 9919bf2 commit 96b0338

File tree

7 files changed

+97
-71
lines changed

7 files changed

+97
-71
lines changed

packages/typescript/README.md

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -300,54 +300,6 @@ export default {
300300

301301
Previous versions of this plugin used Typescript's `transpileModule` API, which is faster but does not perform typechecking and does not support cross-file features like `const enum`s and emit-less types. If you want this behaviour, you can use [@rollup/plugin-sucrase](https://github.com/rollup/plugins/tree/master/packages/sucrase) instead.
302302

303-
### Declaration Output With `output.file`
304-
305-
When instructing Rollup to output a specific file name via the `output.file` Rollup configuration, and TypeScript to output declaration files, users may encounter a situation where the declarations are nested improperly. And additionally when attempting to fix the improper nesting via use of `outDir` or `declarationDir` result in further TypeScript errors.
306-
307-
Consider the following `rollup.config.js` file:
308-
309-
```js
310-
import typescript from '@rollup/plugin-typescript';
311-
312-
export default {
313-
input: 'src/index.ts',
314-
output: {
315-
file: 'dist/index.mjs'
316-
},
317-
plugins: [typescript()]
318-
};
319-
```
320-
321-
And accompanying `tsconfig.json` file:
322-
323-
```json
324-
{
325-
"include": ["*"],
326-
"compilerOptions": {
327-
"outDir": "dist",
328-
"declaration": true
329-
}
330-
}
331-
```
332-
333-
This setup will produce `dist/index.mjs` and `dist/src/index.d.ts`. To correctly place the declaration file, add an `exclude` setting in `tsconfig` and modify the `declarationDir` setting in `compilerOptions` to resemble:
334-
335-
```json
336-
{
337-
"include": ["*"],
338-
"exclude": ["dist"],
339-
"compilerOptions": {
340-
"outDir": "dist",
341-
"declaration": true,
342-
"declarationDir": "."
343-
}
344-
}
345-
```
346-
347-
This will result in the correct output of `dist/index.mjs` and `dist/index.d.ts`.
348-
349-
_For reference, please see the workaround this section is based on [here](https://github.com/microsoft/bistring/commit/7e57116c812ae2c01f383c234f3b447f733b5d0c)_
350-
351303
## Meta
352304

353305
[CONTRIBUTING](/.github/CONTRIBUTING.md)

packages/typescript/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"@rollup/plugin-buble": "^1.0.0",
7272
"@rollup/plugin-commonjs": "^23.0.0",
7373
"@types/node": "^14.18.30",
74+
"@types/resolve": "^1.20.2",
7475
"buble": "^0.20.0",
7576
"rollup": "^3.2.3",
7677
"typescript": "^4.8.3"

packages/typescript/src/index.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -154,24 +154,30 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
154154
const output = findTypescriptOutput(ts, parsedOptions, fileName, emittedFiles, tsCache);
155155
output.declarations.forEach((id) => {
156156
const code = getEmittedFile(id, emittedFiles, tsCache);
157-
let baseDir =
158-
outputOptions.dir ||
159-
(parsedOptions.options.declaration
160-
? parsedOptions.options.declarationDir || parsedOptions.options.outDir
161-
: null);
162-
const cwd = normalizePath(process.cwd());
163-
if (
164-
parsedOptions.options.declaration &&
165-
parsedOptions.options.declarationDir &&
166-
baseDir?.startsWith(cwd)
167-
) {
168-
const declarationDir = baseDir.slice(cwd.length + 1);
169-
baseDir = baseDir.slice(0, -1 * declarationDir.length);
157+
if (!code || !parsedOptions.options.declaration) {
158+
return;
170159
}
171-
if (!baseDir && tsconfig) {
172-
baseDir = tsconfig.substring(0, tsconfig.lastIndexOf('/'));
160+
161+
let baseDir: string | undefined;
162+
if (outputOptions.dir) {
163+
baseDir = outputOptions.dir;
164+
} else if (outputOptions.file) {
165+
// find common path of output.file and configured declation output
166+
const outputDir = path.dirname(outputOptions.file);
167+
const configured = path.resolve(
168+
parsedOptions.options.declarationDir ||
169+
parsedOptions.options.outDir ||
170+
tsconfig ||
171+
process.cwd()
172+
);
173+
const backwards = path
174+
.relative(outputDir, configured)
175+
.split(path.sep)
176+
.filter((v) => v === '..')
177+
.join(path.sep);
178+
baseDir = path.normalize(`${outputDir}/${backwards}`);
173179
}
174-
if (!code || !baseDir) return;
180+
if (!baseDir) return;
175181

176182
this.emitFile({
177183
type: 'asset',

packages/typescript/test/test.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const test = require('ava');
66
const { rollup, watch } = require('rollup');
77
const ts = require('typescript');
88

9-
const { evaluateBundle, getCode, onwarn } = require('../../../util/test');
9+
const { evaluateBundle, getCode, getFiles, onwarn } = require('../../../util/test');
1010

1111
const typescript = require('..');
1212

@@ -139,39 +139,66 @@ test.serial('supports emitting types also for single file output', async (t) =>
139139
// as that would have the side effect that the tsconfig's path would be used as fallback path for
140140
// the here unspecified outputOptions.dir, in which case the original issue wouldn't show.
141141
process.chdir('fixtures/basic');
142+
const outputOpts = { format: 'es', file: 'dist/main.js' };
142143

143144
const warnings = [];
144145
const bundle = await rollup({
145146
input: 'main.ts',
147+
output: outputOpts,
146148
plugins: [typescript({ declaration: true, declarationDir: 'dist' })],
147149
onwarn(warning) {
148150
warnings.push(warning);
149151
}
150152
});
151153
// generate a single output bundle, in which case, declaration files were not correctly emitted
152-
const output = await getCode(bundle, { format: 'es', file: 'dist/main.js' }, true);
154+
const output = await getFiles(bundle, outputOpts);
153155

154156
t.deepEqual(
155157
output.map((out) => out.fileName),
156-
['main.js', 'dist/main.d.ts']
158+
['dist/main.js', 'dist/main.d.ts']
159+
);
160+
t.is(warnings.length, 0);
161+
});
162+
163+
test.serial('supports emitting declarations in correct directory for output.file', async (t) => {
164+
// Ensure even when no `output.dir` is configured, declarations are emitted to configured `declarationDir`
165+
process.chdir('fixtures/basic');
166+
const outputOpts = { format: 'es', file: 'dist/main.esm.js' };
167+
168+
const warnings = [];
169+
const bundle = await rollup({
170+
input: 'main.ts',
171+
output: outputOpts,
172+
plugins: [typescript({ declaration: true, declarationDir: 'dist' })],
173+
onwarn(warning) {
174+
warnings.push(warning);
175+
}
176+
});
177+
const output = await getFiles(bundle, outputOpts);
178+
179+
t.deepEqual(
180+
output.map((out) => out.fileName),
181+
['dist/main.esm.js', 'dist/main.d.ts']
157182
);
158183
t.is(warnings.length, 0);
159184
});
160185

161186
test.serial('relative paths in tsconfig.json are resolved relative to the file', async (t) => {
187+
const outputOpts = { format: 'es', dir: 'fixtures/relative-dir/dist' };
162188
const bundle = await rollup({
163189
input: 'fixtures/relative-dir/main.ts',
190+
output: outputOpts,
164191
plugins: [typescript({ tsconfig: 'fixtures/relative-dir/tsconfig.json' })],
165192
onwarn
166193
});
167-
const output = await getCode(bundle, { format: 'es', dir: 'fixtures/relative-dir/dist' }, true);
194+
const output = await getFiles(bundle, outputOpts);
168195

169196
t.deepEqual(
170197
output.map((out) => out.fileName),
171-
['main.js', 'main.d.ts']
198+
['fixtures/relative-dir/dist/main.js', 'fixtures/relative-dir/dist/main.d.ts']
172199
);
173200

174-
t.true(output[1].source.includes('declare const answer = 42;'), output[1].source);
201+
t.true(output[1].content.includes('declare const answer = 42;'), output[1].content);
175202
});
176203

177204
test.serial('throws for unsupported module types', async (t) => {

pnpm-lock.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

util/test.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ interface GetCode {
1515

1616
export const getCode: GetCode;
1717

18+
export function getFiles(
19+
bundle: RollupBuild,
20+
outputOptions?: OutputOptions
21+
): Promise<
22+
{
23+
fileName: string;
24+
content: any;
25+
}[]
26+
>;
27+
1828
export function evaluateBundle(bundle: RollupBuild): Promise<Pick<NodeModule, 'exports'>>;
1929

2030
export function getImports(bundle: RollupBuild): Promise<string[]>;

util/test.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
const path = require('path');
2+
const process = require('process');
3+
14
/**
25
* @param {import('rollup').RollupBuild} bundle
36
* @param {import('rollup').OutputOptions} [outputOptions]
@@ -7,13 +10,37 @@ const getCode = async (bundle, outputOptions, allFiles = false) => {
710

811
if (allFiles) {
912
return output.map(({ code, fileName, source, map }) => {
10-
return { code, fileName, source, map };
13+
return {
14+
code,
15+
fileName,
16+
source,
17+
map
18+
};
1119
});
1220
}
1321
const [{ code }] = output;
1422
return code;
1523
};
1624

25+
/**
26+
* @param {import('rollup').RollupBuild} bundle
27+
* @param {import('rollup').OutputOptions} [outputOptions]
28+
*/
29+
const getFiles = async (bundle, outputOptions) => {
30+
if (!outputOptions.dir && !outputOptions.file)
31+
throw new Error('You must specify "output.file" or "output.dir" for the build.');
32+
33+
const { output } = await bundle.generate(outputOptions || { format: 'cjs', exports: 'auto' });
34+
35+
return output.map(({ code, fileName, source }) => {
36+
const absPath = path.resolve(outputOptions.dir || path.dirname(outputOptions.file), fileName);
37+
return {
38+
fileName: path.relative(process.cwd(), absPath).split(path.sep).join('/'),
39+
content: code || source
40+
};
41+
});
42+
};
43+
1744
const getImports = async (bundle) => {
1845
if (bundle.imports) {
1946
return bundle.imports;
@@ -70,6 +97,7 @@ const evaluateBundle = async (bundle) => {
7097
module.exports = {
7198
evaluateBundle,
7299
getCode,
100+
getFiles,
73101
getImports,
74102
getResolvedModules,
75103
onwarn,

0 commit comments

Comments
 (0)