Files
wxt/src/core/build/buildEntrypoints.ts
T
2023-07-21 12:57:09 -05:00

165 lines
4.8 KiB
TypeScript

import * as vite from 'vite';
import {
BuildOutput,
BuildStepOutput,
Entrypoint,
EntrypointGroup,
InternalConfig,
} from '../types';
import * as wxtPlugins from '../vite-plugins';
import { removeEmptyDirs } from '../utils/removeEmptyDirs';
import { getEntrypointBundlePath } from '../utils/entrypoints';
import fs from 'fs-extra';
import { dirname, resolve } from 'path';
import { getPublicFiles } from '../utils/public';
export async function buildEntrypoints(
groups: EntrypointGroup[],
config: InternalConfig,
): Promise<Omit<BuildOutput, 'manifest'>> {
const steps: BuildStepOutput[] = [];
for (const group of groups) {
const step = Array.isArray(group)
? await buildMultipleEntrypoints(group, config)
: await buildSingleEntrypoint(group, config);
steps.push(step);
}
const publicAssets = await copyPublicDirectory(config);
// Remove any empty directories from moving outputs around
await removeEmptyDirs(config.outDir);
return { publicAssets, steps };
}
/**
* Use Vite's lib mode + IIFE format to bundle the entrypoint to a single file.
*/
async function buildSingleEntrypoint(
entrypoint: Entrypoint,
config: InternalConfig,
): Promise<BuildStepOutput> {
// Should this entrypoint be wrapped by the vite-plugins/virtualEntrypoint plugin?
const isVirtual = ['background', 'content-script'].includes(entrypoint.type);
const entry = isVirtual
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
: entrypoint.inputPath;
const plugins: NonNullable<vite.UserConfig['plugins']> = [];
if (
entrypoint.type === 'content-script-style' ||
entrypoint.type === 'unlisted-style'
) {
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, config));
}
const libMode: vite.UserConfig = {
plugins,
build: {
lib: {
entry,
formats: ['iife'],
name: '_',
fileName: entrypoint.name,
},
rollupOptions: {
output: {
// There's only a single output for this build, so we use the desired bundle path for the
// entry output (like "content-scripts/overlay.js")
entryFileNames: getEntrypointBundlePath(
entrypoint,
config.outDir,
'.js',
),
// Output content script CSS to assets/ with a hash to prevent conflicts. Defaults to
// "[name].[ext]" in lib mode, which usually results in "style.css". That means multiple
// content scripts with styles would overwrite each other if it weren't changed below.
assetFileNames: `assets/${entrypoint.name}.[ext]`,
},
},
},
};
const entryConfig = vite.mergeConfig(
libMode,
config.vite,
) as vite.InlineConfig;
const result = await vite.build(entryConfig);
return {
entrypoints: entrypoint,
chunks: getBuildOutputChunks(result),
};
}
/**
* Use Vite's multipage build to bundle all the entrypoints in a single step.
*/
async function buildMultipleEntrypoints(
entrypoints: Entrypoint[],
config: InternalConfig,
): Promise<BuildStepOutput> {
const multiPage: vite.UserConfig = {
plugins: [wxtPlugins.multipageMove(entrypoints, config)],
build: {
rollupOptions: {
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
input[entry.name] = entry.inputPath;
return input;
}, {}),
output: {
// Include a hash to prevent conflicts
chunkFileNames: 'chunks/[name]-[hash].js',
// Include a hash to prevent conflicts
entryFileNames: 'chunks/[name]-[hash].js',
// We can't control the "name", so we need a hash to prevent conflicts
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
},
};
const entryConfig = vite.mergeConfig(
multiPage,
config.vite,
) as vite.UserConfig;
const result = await vite.build(entryConfig);
return {
entrypoints,
chunks: getBuildOutputChunks(result),
};
}
function getBuildOutputChunks(
result: Awaited<ReturnType<typeof vite.build>>,
): BuildStepOutput['chunks'] {
if ('on' in result) throw Error('wxt does not support vite watch mode.');
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
return result.output;
}
async function copyPublicDirectory(
config: InternalConfig,
): Promise<BuildOutput['publicAssets']> {
const files = await getPublicFiles(config);
if (files.length === 0) return [];
const publicAssets: BuildOutput['publicAssets'] = [];
for (const file of files) {
const srcPath = resolve(config.publicDir, file);
const outPath = resolve(config.outDir, file);
await fs.ensureDir(dirname(outPath));
await fs.copyFile(srcPath, outPath);
publicAssets.push({
type: 'asset',
fileName: file,
name: file,
needsCodeReference: false,
source: await fs.readFile(srcPath),
});
}
return publicAssets;
}