Skip to content

Commit

Permalink
feat: generate correct declarations, diagnostics, and .tsbuildinfo wh…
Browse files Browse the repository at this point in the history
…en a Rollup cache is being used. Fixes #140
  • Loading branch information
wessberg committed Apr 14, 2022
1 parent 16d24fa commit afa76ed
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 36 deletions.
40 changes: 35 additions & 5 deletions src/plugin/typescript-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ExistingRawSourceMap, InputOptions, OutputBundle, OutputOptions, Plugin, PluginContext, RenderedChunk, SourceDescription} from "rollup";
import {ExistingRawSourceMap, InputOptions, OutputBundle, OutputOptions, Plugin, PluginContext, RenderedChunk, RollupCache, SourceDescription} from "rollup";
import {getParsedCommandLine} from "../util/get-parsed-command-line/get-parsed-command-line";
import {getForcedCompilerOptions} from "../util/get-forced-compiler-options/get-forced-compiler-options";
import {getSourceDescriptionFromEmitOutput} from "../util/get-source-description-from-emit-output/get-source-description-from-emit-output";
Expand Down Expand Up @@ -29,6 +29,7 @@ import path from "crosspath";
import {loadBabel, loadSwc} from "../util/transpiler-loader";
import {BabelConfigFactory, getBabelConfig, getDefaultBabelOptions, getForcedBabelOptions, replaceBabelHelpers} from "../transpiler/babel";
import {getSwcConfigFactory, SwcConfigFactory} from "../transpiler/swc";
import {inputOptionsAreEqual} from "../util/rollup/rollup-util";

/**
* The name of the Rollup plugin
Expand Down Expand Up @@ -97,6 +98,11 @@ export default function typescriptRollupPlugin(pluginInputOptions: Partial<Types
*/
let rollupInputOptions: InputOptions;

/**
* The previously emitted Rollup cache used as input, if any
*/
let inputCache: RollupCache | undefined;

/**
* A Set of the entry filenames for when using rollup-plugin-multi-entry (we need to track this for generating valid declarations)
*/
Expand All @@ -107,7 +113,7 @@ export default function typescriptRollupPlugin(pluginInputOptions: Partial<Types
*/
let MULTI_ENTRY_MODULE: string | undefined;

const addAndEmitFile = (fileName: string, text: string, dependencyCb: (dependency: string) => void): SourceDescription | undefined => {
const addFile = (fileName: string, text: string, dependencyCb: (dependency: string) => void): void => {
// Add the file to the CompilerHost
host.add({fileName, text, fromRollup: true});

Expand All @@ -121,6 +127,10 @@ export default function typescriptRollupPlugin(pluginInputOptions: Partial<Types
dependencyCb(pickedDependency);
}
}
};

const addAndEmitFile = (fileName: string, text: string, dependencyCb: (dependency: string) => void): SourceDescription | undefined => {
addFile(fileName, text, dependencyCb);

// Get some EmitOutput, optionally from the cache if the file contents are unchanged
const emitOutput = host.emit(fileName, false);
Expand All @@ -136,10 +146,13 @@ export default function typescriptRollupPlugin(pluginInputOptions: Partial<Types
* Invoked when Input options has been received by Rollup
*/
async options(options: InputOptions): Promise<undefined> {
// Break if the options aren't different from the previous ones
if (rollupInputOptions != null) return;
// Always update the input cache
inputCache = typeof options.cache === "boolean" ? undefined : options.cache;

// Don't proceed if the options are identical to the previous ones
if (rollupInputOptions != null && inputOptionsAreEqual(rollupInputOptions, options)) return;

// Re-assign the input options
// Re-assign the full input options
rollupInputOptions = options;

const multiEntryPlugin = options.plugins?.find(plugin => plugin != null && typeof plugin !== "boolean" && plugin.name === "multi-entry");
Expand Down Expand Up @@ -462,6 +475,23 @@ export default function typescriptRollupPlugin(pluginInputOptions: Partial<Types
* from the LanguageService
*/
generateBundle(this: PluginContext, outputOptions: OutputOptions, bundle: OutputBundle): void {
// If a cache was provided to Rollup,
// some or all files may not have been added to the CompilerHost
// and therefore it will not be possible to compile correct diagnostics,
// declarations, and/or .buildinfo. To work around this, we'll have to make sure
// all files that are part of the compilation unit is in fact added to the CompilerHost
if (inputCache != null) {
for (const module of inputCache.modules) {
const normalizedFile = path.normalize(module.id);

// Don't proceed if we already know about that file
if (host.has(normalizedFile)) continue;

// Add to the CompilerHost
addFile(normalizedFile, module.originalCode, dependency => this.addWatchFile(dependency));
}
}

// If debugging is active, log the outputted files
for (const file of Object.values(bundle)) {
if (!("fileName" in file)) continue;
Expand Down
49 changes: 49 additions & 0 deletions src/util/rollup/rollup-util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {InputOptions} from "rollup";

interface EqualResult {
equal: true;
}

interface NotEqualResult {
equal: false;
path: string[];
}

type EqualityResult = EqualResult | NotEqualResult;

const ignoredKeys = new Set(["cache"]);

/**
* Treat the options as equal if their properties are somewhat equal, not taking their cache into account
*/
export function inputOptionsAreEqual(a: InputOptions, b: InputOptions): boolean {
return inputValuesAreEqual(a, b).equal;
}

function inputValuesAreEqual(a: unknown, b: unknown, path: string[] = []): EqualityResult {
if (a === b || (a == null && b == null)) return {equal: true};
else if (typeof a !== typeof b) return {equal: false, path};
else if (Array.isArray(a)) {
if (!Array.isArray(b)) return {equal: false, path};
else if (a.length !== b.length) return {equal: false, path};
else if (a.some((element, index) => !inputValuesAreEqual(element, b[index], [...path, String(index)]).equal)) {
return {equal: false, path};
} else {
return {equal: true};
}
} else if (typeof a === "object" && a != null) {
if (typeof b !== "object" || b == null) return {equal: false, path};

const aKeys = Object.keys(a).filter(key => !ignoredKeys.has(key));
const bKeys = Object.keys(b).filter(key => !ignoredKeys.has(key));

if (aKeys.length !== bKeys.length) return {equal: false, path};
else if (aKeys.some(key => !inputValuesAreEqual(a[key as keyof typeof a], b[key as keyof typeof b], [...path, key]).equal)) {
return {equal: false, path};
} else {
return {equal: true};
}
} else {
return {equal: true};
}
}
30 changes: 30 additions & 0 deletions test/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import test from "ava";
import {withTypeScript} from "./util/ts-macro";
import {formatCode} from "./util/format-code";
import {generateRollupBundle} from "./setup/setup-rollup";

test.serial("Declaration bundling works properly when Rollup is using a cache. #1", withTypeScript, async (t, {typescript}) => {
const bundle = await generateRollupBundle(
[
{
entry: true,
fileName: "index.ts",
text: `\
export default () => 42
`
}
],
{typescript, runCachedBuild: true, debug: false}
);
const {
declarations: [file]
} = bundle;

t.deepEqual(
formatCode(file.code),
formatCode(`\
declare const _default: () => number;
export { _default as default };
`)
);
});
78 changes: 47 additions & 31 deletions test/setup/setup-rollup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Plugin, rollup, RollupOptions, RollupOutput} from "rollup";
import {Plugin, rollup, RollupBuild, RollupCache, RollupOptions, RollupOutput} from "rollup";
import commonjs from "@rollup/plugin-commonjs";
import typescriptRollupPlugin from "../../src/plugin/typescript-plugin";
import {HookRecord, InputCompilerOptions, TypescriptPluginBabelOptions, TypescriptPluginOptions, TypescriptPluginSwcOptions} from "../../src/plugin/typescript-plugin-options";
Expand Down Expand Up @@ -42,6 +42,7 @@ export interface GenerateRollupBundleOptions {
browserslist: TypescriptPluginOptions["browserslist"];
chunkFileNames: string;
entryFileNames: string;
runCachedBuild: boolean;
}

/**
Expand All @@ -52,6 +53,7 @@ export async function generateRollupBundle(
{
rollupOptions = {},
format = "esm",
runCachedBuild = false,
prePlugins = [],
postPlugins = [],
entryFileNames,
Expand Down Expand Up @@ -141,37 +143,51 @@ export async function generateRollupBundle(
}
}

const result = await rollup({
input,
...rollupOptions,
onwarn: (warning, defaultHandler) => {
// Eat all irrelevant Rollup warnings (such as 'Generated an empty chunk: "index") while running tests
if (!warning.message.includes("Generated an empty chunk") && !warning.message.includes(`Circular dependency:`) && !warning.message.includes(`Conflicting namespaces:`)) {
defaultHandler(warning);
}
},
plugins: [
{
name: "VirtualFileResolver",
resolveId,
load
let cache: RollupCache | undefined;
let result: RollupBuild | undefined;

while (true) {
result = await rollup({
input,
cache,
...rollupOptions,
onwarn: (warning, defaultHandler) => {
// Eat all irrelevant Rollup warnings (such as 'Generated an empty chunk: "index") while running tests
if (!warning.message.includes("Generated an empty chunk") && !warning.message.includes(`Circular dependency:`) && !warning.message.includes(`Conflicting namespaces:`)) {
defaultHandler(warning);
}
},
commonjs(),
...prePlugins,
typescriptRollupPlugin({
...context,
fileSystem,
transformers,
browserslist,
babelConfig,
swcConfig
}),
...(rollupOptions.plugins == null ? [] : rollupOptions.plugins),
...postPlugins
]
});
plugins: [
{
name: "VirtualFileResolver",
resolveId,
load
},
commonjs(),
...prePlugins,
typescriptRollupPlugin({
...context,
fileSystem,
transformers,
browserslist,
babelConfig,
swcConfig
}),
...(rollupOptions.plugins == null ? [] : rollupOptions.plugins),
...postPlugins
]
});

if (runCachedBuild && cache == null) {
cache = result.cache;

// Run again, this time with caching
continue;
} else {
break;
}
}

const extraFiles: {type: "chunk" | "asset"; source: string; fileName: string}[] = [];
const bundle = await result.generate({
dir: context.dist,
format,
Expand All @@ -180,7 +196,7 @@ export async function generateRollupBundle(
...(chunkFileNames == null ? {} : {chunkFileNames})
});

for (const file of [...bundle.output, ...extraFiles]) {
for (const file of bundle.output) {
if (file.type === "chunk" && "code" in file) {
js.push({
code: file.code,
Expand Down

0 comments on commit afa76ed

Please sign in to comment.