wxt
Version:
⚡ Next-gen Web Extension Framework
134 lines (133 loc) • 6.25 kB
JavaScript
import { normalizePath } from "./utils/paths.mjs";
import "./utils/index.mjs";
import { safeFilename } from "./utils/strings.mjs";
import { registerWxt, wxt } from "./wxt.mjs";
import { findEntrypoints } from "./utils/building/find-entrypoints.mjs";
import { formatDuration } from "./utils/time.mjs";
import { printFileList } from "./utils/log/printFileList.mjs";
import "./utils/log/index.mjs";
import { getPackageJson } from "./utils/package.mjs";
import { internalBuild } from "./utils/building/internal-build.mjs";
import "./utils/building/index.mjs";
import { mkdir, readFile } from "node:fs/promises";
import { glob } from "tinyglobby";
import path from "node:path";
import { createWriteStream } from "node:fs";
import JSZip from "jszip";
//#region src/core/zip.ts
/**
* Build and zip the extension for distribution.
*
* @param config Optional config that will override your `<root>/wxt.config.ts`.
* @returns A list of all files included in the ZIP.
*/
async function zip(config) {
await registerWxt("build", config);
const output = await internalBuild();
await wxt.hooks.callHook("zip:start", wxt);
const start = Date.now();
wxt.logger.info("Zipping extension...");
const zipFiles = [];
const packageJson = await getPackageJson();
const projectName = wxt.config.zip.name ?? safeFilename(packageJson?.name || path.basename(process.cwd()));
const modeSuffix = {
production: "",
development: "-dev"
}[wxt.config.mode] ?? `-${wxt.config.mode}`;
const applyTemplate = (template) => template.replaceAll("{{name}}", projectName).replaceAll("{{browser}}", wxt.config.browser).replaceAll("{{version}}", output.manifest.version).replaceAll("{{versionName}}", output.manifest.version_name ?? output.manifest.version).replaceAll("{{packageVersion}}", packageJson?.version).replaceAll("{{modeSuffix}}", modeSuffix).replaceAll("{{mode}}", wxt.config.mode).replaceAll("{{manifestVersion}}", `mv${wxt.config.manifestVersion}`);
await mkdir(wxt.config.outBaseDir, { recursive: true });
await wxt.hooks.callHook("zip:extension:start", wxt);
const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate);
const outZipPath = path.resolve(wxt.config.outBaseDir, outZipFilename);
await zipDir(wxt.config.outDir, outZipPath, { exclude: wxt.config.zip.exclude });
zipFiles.push(outZipPath);
await wxt.hooks.callHook("zip:extension:done", wxt, outZipPath);
if (wxt.config.zip.zipSources) {
const skippedEntrypoints = (await findEntrypoints()).filter((entry) => entry.skipped);
const excludeSources = [...wxt.config.zip.excludeSources, ...skippedEntrypoints.map((entry) => path.relative(wxt.config.zip.sourcesRoot, entry.inputPath))].map((paths) => paths.replaceAll("\\", "/"));
await wxt.hooks.callHook("zip:sources:start", wxt);
const { overrides, files: downloadedPackages } = await downloadPrivatePackages();
const sourcesZipFilename = applyTemplate(wxt.config.zip.sourcesTemplate);
const sourcesZipPath = path.resolve(wxt.config.outBaseDir, sourcesZipFilename);
const files = await zipDir(wxt.config.zip.sourcesRoot, sourcesZipPath, {
include: wxt.config.zip.includeSources,
exclude: excludeSources,
transform(absolutePath, zipPath, content) {
if (zipPath.endsWith("package.json")) return addOverridesToPackageJson(absolutePath, content, overrides);
},
additionalFiles: downloadedPackages,
dot: wxt.config.zip.dotSources
});
zipFiles.push(sourcesZipPath);
await wxt.hooks.callHook("zip:sources:done", wxt, sourcesZipPath);
await printFileList(wxt.logger.info, `Sources included in \`${sourcesZipFilename}\``, wxt.config.zip.sourcesRoot, files);
}
await printFileList(wxt.logger.success, `Zipped extension in ${formatDuration(Date.now() - start)}`, wxt.config.outBaseDir, zipFiles);
await wxt.hooks.callHook("zip:done", wxt, zipFiles);
return zipFiles;
}
async function zipDir(directory, outputPath, options) {
const archive = new JSZip();
const filesToZip = [...await glob(options?.include ?? ["**/*"], {
cwd: directory,
ignore: options?.exclude ?? [],
onlyFiles: true,
dot: options?.dot
}), ...(options?.additionalFiles ?? []).map((file) => path.relative(directory, file))].sort();
for (const file of filesToZip) {
const absolutePath = path.resolve(directory, file);
if (file.endsWith(".json")) {
const content = await readFile(absolutePath, "utf-8");
archive.file(file, await options?.transform?.(absolutePath, file, content) || content);
} else {
const content = await readFile(absolutePath);
archive.file(file, content);
}
}
await options?.additionalWork?.(archive);
await new Promise((resolve, reject) => archive.generateNodeStream({
type: "nodebuffer",
...wxt.config.zip.compressionLevel === 0 ? { compression: "STORE" } : {
compression: "DEFLATE",
compressionOptions: { level: wxt.config.zip.compressionLevel }
}
}).pipe(createWriteStream(outputPath)).on("error", reject).on("close", resolve));
return filesToZip;
}
async function downloadPrivatePackages() {
const overrides = {};
const files = [];
if (wxt.config.zip.downloadPackages.length > 0) {
const _downloadPackages = new Set(wxt.config.zip.downloadPackages);
const downloadPackages = (await wxt.pm.listDependencies({
all: true,
cwd: wxt.config.root
})).filter((pkg) => _downloadPackages.has(pkg.name));
for (const pkg of downloadPackages) {
wxt.logger.info(`Downloading package: ${pkg.name}@${pkg.version}`);
const id = `${pkg.name}@${pkg.version}`;
const tgzPath = await wxt.pm.downloadDependency(id, wxt.config.zip.downloadedPackagesDir);
files.push(tgzPath);
overrides[id] = tgzPath;
}
}
return {
overrides,
files
};
}
function addOverridesToPackageJson(absolutePackageJsonPath, content, overrides) {
if (Object.keys(overrides).length === 0) return content;
const packageJsonDir = path.dirname(absolutePackageJsonPath);
const oldPackage = JSON.parse(content);
const newPackage = {
...oldPackage,
[wxt.pm.overridesKey]: { ...oldPackage[wxt.pm.overridesKey] }
};
Object.entries(overrides).forEach(([key, absolutePath]) => {
newPackage[wxt.pm.overridesKey][key] = "file://./" + normalizePath(path.relative(packageJsonDir, absolutePath));
});
return JSON.stringify(newPackage, null, 2);
}
//#endregion
export { zip };