Files
squoosh/lib/client-bundle-plugin.js
Jake Archibald 56e10b3aa2 Rollup build
2020-11-19 11:00:23 +00:00

176 lines
5.5 KiB
JavaScript

/**
* Copyright 2020 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import rollup from 'rollup';
const prefix = 'client-bundle:';
const entryPathPlaceholder = 'CLIENT_BUNDLE_PLUGIN_ENTRY_PATH';
const importsPlaceholder = 'CLIENT_BUNDLE_PLUGIN_IMPORTS';
const allSrcPlaceholder = 'CLIENT_BUNDLE_PLUGIN_ALL_SRC';
export function getDependencies(clientOutput, item) {
const crawlDependencies = new Set([item.fileName]);
for (const fileName of crawlDependencies) {
const chunk = clientOutput.find((v) => v.fileName === fileName);
for (const dep of chunk.imports) {
crawlDependencies.add(dep);
}
}
// Don't add self as dependency
crawlDependencies.delete(item.fileName);
return [...crawlDependencies];
}
export default function (inputOptions, outputOptions, resolveFileUrl) {
let cache;
let entryPointPlaceholderMap;
let exportCounter;
let clientBundle;
let clientOutput;
return {
name: 'client-bundle',
buildStart() {
entryPointPlaceholderMap = new Map();
exportCounter = 0;
},
async resolveId(id, importer) {
if (!id.startsWith(prefix)) return null;
const realId = id.slice(prefix.length);
const resolveResult = await this.resolve(realId, importer);
// Add an additional .js to the end so it ends up with .js at the end in the _virtual folder.
if (resolveResult) return prefix + resolveResult.id + '.js';
// This Rollup couldn't resolve it, but maybe the inner one can.
return id + '.js';
},
load(id) {
if (!id.startsWith(prefix)) return;
const realId = id.slice(prefix.length, -'.js'.length);
exportCounter++;
entryPointPlaceholderMap.set(exportCounter, realId);
return [
`export default import.meta.${entryPathPlaceholder + exportCounter};`,
`export const imports = import.meta.${
importsPlaceholder + exportCounter
};`,
`export const allSrc = import.meta.${
allSrcPlaceholder + exportCounter
};`,
].join('\n');
},
async buildEnd(error) {
const entryPoints = [...entryPointPlaceholderMap.values()];
// The static-build is done, so now we can perform our client build.
// Exit early if there's nothing to build.
if (error || entryPoints.length === 0) return;
clientBundle = await rollup.rollup({
...inputOptions,
cache,
input: entryPoints,
});
cache = clientBundle.cache;
},
async renderStart(staticBuildOutputOpts) {
// The static-build has started generating output, so we can do the same for our client build.
// Exit early if there's nothing to build.
if (!clientBundle) return;
const copiedOutputOptions = {
assetFileNames: staticBuildOutputOpts.assetFileNames,
};
clientOutput = (
await clientBundle.generate({
...copiedOutputOptions,
...outputOptions,
})
).output;
},
resolveImportMeta(property, { moduleId, format }) {
// Pick up the placeholder exports we created earlier, and fill in the correct details.
let num = undefined;
if (property.startsWith(entryPathPlaceholder)) {
num = Number(property.slice(entryPathPlaceholder.length));
} else if (property.startsWith(importsPlaceholder)) {
num = Number(property.slice(importsPlaceholder.length));
} else if (property.startsWith(allSrcPlaceholder)) {
num = Number(property.slice(allSrcPlaceholder.length));
} else {
// This isn't one of our placeholders.
return;
}
const id = entryPointPlaceholderMap.get(num);
const clientEntry = clientOutput.find(
(item) => item.facadeModuleId === id,
);
if (property.startsWith(entryPathPlaceholder)) {
return resolveFileUrl({
fileName: clientEntry.fileName,
moduleId,
format,
});
}
const dependencies = getDependencies(clientOutput, clientEntry);
if (property.startsWith(allSrcPlaceholder)) {
return JSON.stringify(
[clientEntry.code, ...dependencies.map((d) => d.code)].join(';'),
);
}
return (
'[' +
dependencies
.map((item) => {
const entry = clientOutput.find((v) => v.fileName === item);
return resolveFileUrl({
fileName: entry.fileName,
moduleId,
format: outputOptions.format,
});
})
.join(',') +
']'
);
},
async generateBundle(options, bundle) {
// Exit early if there's nothing to build.
if (!clientOutput) return;
// Copy everything from the client bundle into the main bundle.
for (const clientEntry of clientOutput) {
// Skip if the file already exists
if (clientEntry.fileName in bundle) continue;
this.emitFile({
type: 'asset',
source: clientEntry.code || clientEntry.source,
fileName: clientEntry.fileName,
});
}
},
};
}