vike
Version:
The Framework *You* Control - Next.js & Nuxt alternative for unprecedented flexibility and dependability.
331 lines (330 loc) • 15.1 kB
JavaScript
export { handleAssetsManifest };
export { handleAssetsManifest_getBuildConfig };
export { handleAssetsManifest_isFixEnabled };
export { handleAssetsManifest_assertUsageCssCodeSplit };
export { handleAssetsManifest_assertUsageCssTarget };
import fs from 'node:fs/promises';
import fs_sync from 'node:fs';
import path from 'node:path';
import { existsSync } from 'node:fs';
import { assert, assertIsSingleModuleInstance, assertWarning, isEqualStringList, isObject, pLimit, unique, } from '../../utils.js';
import { isVirtualFileIdPageConfigLazy } from '../../../shared/virtualFiles/virtualFilePageConfigLazy.js';
import { manifestTempFile } from './pluginBuildConfig.js';
import { getAssetsDir } from '../../shared/getAssetsDir.js';
import pc from '@brillout/picocolors';
import { getVikeConfigInternal, isV1Design } from '../../shared/resolveVikeConfigInternal.js';
import { getOutDirs } from '../../shared/getOutDirs.js';
import { isViteServerBuild_onlySsrEnv, isViteServerBuild } from '../../shared/isViteServerBuild.js';
import { set_macro_ASSETS_MANIFEST } from './pluginBuildEntry.js';
assertIsSingleModuleInstance('build/handleAssetsManifest.ts');
let assetsJsonFilePath;
// true => use workaround config.build.ssrEmitAssets
// false => use workaround extractAssets plugin
function handleAssetsManifest_isFixEnabled(config) {
// Allow user to toggle between the two workarounds? E.g. based on https://vike.dev/includeAssetsImportedByServer.
return isV1Design();
}
/** https://github.com/vikejs/vike/issues/1339 */
async function fixServerAssets(config) {
const outDirs = getOutDirs(config);
const clientManifest = await readManifestFile(outDirs.outDirClient);
const serverManifest = await readManifestFile(outDirs.outDirServer);
const { clientManifestMod, serverManifestMod, filesToMove, filesToRemove } = addServerAssets(clientManifest, serverManifest);
await copyAssets(filesToMove, filesToRemove, config);
return { clientManifestMod, serverManifestMod };
}
async function copyAssets(filesToMove, filesToRemove, config) {
const { outDirClient, outDirServer } = getOutDirs(config);
const assetsDir = getAssetsDir(config);
const assetsDirServer = path.posix.join(outDirServer, assetsDir);
if (!filesToMove.length && !filesToRemove.length && !existsSync(assetsDirServer))
return;
assert(existsSync(assetsDirServer));
const concurrencyLimit = pLimit(10);
await Promise.all(filesToMove.map((file) => concurrencyLimit(async () => {
const source = path.posix.join(outDirServer, file);
const target = path.posix.join(outDirClient, file);
await fs.mkdir(path.posix.dirname(target), { recursive: true });
await fs.rename(source, target);
})));
filesToRemove.forEach((file) => {
const filePath = path.posix.join(outDirServer, file);
fs_sync.unlinkSync(filePath);
});
/* We cannot do that because, with some edge case Rollup settings (outputting JavaScript chunks and static assets to the same directory), this removes JavaScript chunks, see https://github.com/vikejs/vike/issues/1154#issuecomment-1975762404
await fs.rm(assetsDirServer, { recursive: true })
*/
removeEmptyDirectories(assetsDirServer);
}
// Add serverManifest resources to clientManifest
function addServerAssets(clientManifest, serverManifest) {
var _a, _b, _c, _d;
const entriesClient = new Map();
const entriesServer = new Map();
for (const [key, entry] of Object.entries(clientManifest)) {
const pageId = getPageId(key);
if (!pageId)
continue;
const resources = collectResources(entry, clientManifest);
assert(!entriesClient.has(pageId));
entriesClient.set(pageId, { key, ...resources });
}
for (const [key, entry] of Object.entries(serverManifest)) {
const pageId = getPageId(key);
if (!pageId)
continue;
const resources = collectResources(entry, serverManifest);
assert(!entriesServer.has(pageId));
entriesServer.set(pageId, { key, ...resources });
}
let filesToMove = [];
let filesToRemove = [];
// Copy page assets
for (const [pageId, entryClient] of entriesClient.entries()) {
const entryServer = entriesServer.get(pageId);
if (!entryServer)
continue;
const cssToMove = [];
const cssToRemove = [];
const assetsToMove = [];
const assetsToRemove = [];
entryServer.css.forEach((cssServer) => {
if (!entryClient.css.some((cssClient) => cssServer.hash === cssClient.hash)) {
cssToMove.push(cssServer.src);
}
else {
cssToRemove.push(cssServer.src);
}
});
entryServer.assets.forEach((assetServer) => {
if (!entryClient.assets.some((assetClient) => assetServer.hash === assetClient.hash)) {
assetsToMove.push(assetServer.src);
}
else {
assetsToRemove.push(assetServer.src);
}
});
if (cssToMove.length) {
const { key } = entryClient;
filesToMove.push(...cssToMove);
(_a = clientManifest[key]).css ?? (_a.css = []);
clientManifest[key].css?.push(...cssToMove);
}
if (cssToRemove.length) {
const { key } = entryServer;
filesToRemove.push(...cssToRemove);
(_b = serverManifest[key]).css ?? (_b.css = []);
serverManifest[key].css = serverManifest[key].css.filter((entry) => !cssToRemove.includes(entry));
}
if (assetsToMove.length) {
const { key } = entryClient;
filesToMove.push(...assetsToMove);
(_c = clientManifest[key]).assets ?? (_c.assets = []);
clientManifest[key].assets?.push(...assetsToMove);
}
if (assetsToRemove.length) {
const { key } = entryServer;
filesToRemove.push(...assetsToRemove);
(_d = serverManifest[key]).assets ?? (_d.assets = []);
serverManifest[key].assets = serverManifest[key].assets.filter((entry) => !assetsToRemove.includes(entry));
}
}
// Also copy assets of virtual:@brillout/vite-plugin-server-entry:serverEntry
{
const filesClientAll = [];
for (const key in clientManifest) {
const entry = clientManifest[key];
filesClientAll.push(entry.file);
filesClientAll.push(...(entry.assets ?? []));
filesClientAll.push(...(entry.css ?? []));
}
for (const key in serverManifest) {
const entry = serverManifest[key];
if (!entry.isEntry)
continue;
const resources = collectResources(entry, serverManifest);
const css = resources.css.map((css) => css.src).filter((file) => !filesClientAll.includes(file));
const assets = resources.assets.map((asset) => asset.src).filter((file) => !filesClientAll.includes(file));
filesToMove.push(...css, ...assets);
if (css.length > 0 || assets.length > 0) {
assert(!clientManifest[key]);
clientManifest[key] = {
...entry,
css,
assets,
dynamicImports: undefined,
imports: undefined,
};
}
}
}
const clientManifestMod = clientManifest;
const serverManifestMod = serverManifest;
filesToMove = unique(filesToMove);
filesToRemove = unique(filesToRemove).filter((file) => !filesToMove.includes(file));
return { clientManifestMod, serverManifestMod, filesToMove, filesToRemove };
}
function getPageId(key) {
// Normalize from:
// ../../virtual:vike:pageConfigLazy:client:/pages/index
// to:
// virtual:vike:pageConfigLazy:client:/pages/index
// (This seems to be needed only for vitest tests that use Vite's build() API with an inline config.)
key = key.substring(key.indexOf('virtual:vike'));
const result = isVirtualFileIdPageConfigLazy(key);
return result && result.pageId;
}
function collectResources(entryRoot, manifest) {
const css = [];
const assets = [];
const entries = new Set([entryRoot]);
for (const entry of entries) {
for (const entryImport of entry.imports ?? []) {
entries.add(manifest[entryImport]);
}
const entryCss = entry.css ?? [];
if (entry.file.endsWith('.css'))
entryCss.push(entry.file);
for (const src of entryCss) {
const hash = getHash(src);
css.push({ src, hash });
}
const entryAssets = entry.assets ?? [];
for (const src of entryAssets) {
const hash = getHash(src);
assets.push({ src, hash });
}
}
return { css, assets };
}
// Use the hash of resources to determine whether they are equal. We need this, otherwise we get:
// ```html
// <head>
// <link rel="stylesheet" type="text/css" href="/assets/static/onRenderClient.2j6TxKIB.css">
// <link rel="stylesheet" type="text/css" href="/assets/static/onRenderHtml.2j6TxKIB.css">
// </head>
// ```
function getHash(src) {
// src is guaranteed to end with `.[hash][extname]`, see pluginDistFileNames.ts
const hash = src.split('.').at(-2);
assert(hash);
return hash;
}
// https://github.com/vikejs/vike/issues/1993
function handleAssetsManifest_assertUsageCssCodeSplit(config) {
if (!handleAssetsManifest_isFixEnabled(config))
return;
assertWarning(config.build.cssCodeSplit, `${pc.cyan('build.cssCodeSplit')} shouldn't be set to ${pc.cyan('false')} (https://github.com/vikejs/vike/issues/1993)`, { onlyOnce: true });
}
const targets = [];
function handleAssetsManifest_assertUsageCssTarget(config) {
if (!handleAssetsManifest_isFixEnabled(config))
return;
const isServerSide = isViteServerBuild(config);
assert(typeof isServerSide === 'boolean');
assert(config.build.target !== undefined);
targets.push({ global: config.build.target, css: config.build.cssTarget, isServerSide });
const targetsServer = targets.filter((t) => t.isServerSide);
const targetsClient = targets.filter((t) => !t.isServerSide);
targetsClient.forEach((targetClient) => {
const targetCssResolvedClient = resolveCssTarget(targetClient);
targetsServer.forEach((targetServer) => {
const targetCssResolvedServer = resolveCssTarget(targetServer);
assertWarning(isEqualStringList(targetCssResolvedClient, targetCssResolvedServer), [
'The CSS browser target should be the same for both client and server, but we got:',
`Client: ${pc.cyan(JSON.stringify(targetCssResolvedClient))}`,
`Server: ${pc.cyan(JSON.stringify(targetCssResolvedServer))}`,
`Different targets lead to CSS duplication, see ${pc.underline('https://github.com/vikejs/vike/issues/1815#issuecomment-2507002979')} for more information.`,
].join('\n'), {
showStackTrace: true,
onlyOnce: 'different-css-target',
});
});
});
}
function resolveCssTarget(target) {
return target.css ?? target.global;
}
/**
* Recursively remove all empty directories in a given directory.
*/
function removeEmptyDirectories(dirPath) {
// Read the directory contents
const files = fs_sync.readdirSync(dirPath);
// Iterate through the files and subdirectories
for (const file of files) {
const fullPath = path.join(dirPath, file);
// Check if it's a directory
if (fs_sync.statSync(fullPath).isDirectory()) {
// Recursively clean up the subdirectory
removeEmptyDirectories(fullPath);
}
}
// Re-check the directory; remove it if it's now empty
if (fs_sync.readdirSync(dirPath).length === 0) {
fs_sync.rmdirSync(dirPath);
}
}
async function readManifestFile(outDir) {
const manifestFilePath = path.posix.join(outDir, manifestTempFile);
const manifestFileContent = await fs.readFile(manifestFilePath, 'utf-8');
assert(manifestFileContent);
const manifest = JSON.parse(manifestFileContent);
assert(manifest);
assert(isObject(manifest));
return manifest;
}
async function writeManifestFile(manifest, manifestFilePath) {
assert(isObject(manifest));
const manifestFileContent = JSON.stringify(manifest, null, 2);
await fs.writeFile(manifestFilePath, manifestFileContent, 'utf-8');
}
async function handleAssetsManifest_getBuildConfig(config) {
const vikeConfig = await getVikeConfigInternal();
const isFixEnabled = handleAssetsManifest_isFixEnabled(config);
return {
// https://github.com/vikejs/vike/issues/1339
ssrEmitAssets: isFixEnabled ? true : undefined,
// Required if `ssrEmitAssets: true`, see https://github.com/vitejs/vite/pull/11430#issuecomment-1454800934
cssMinify: isFixEnabled ? 'esbuild' : undefined,
manifest: manifestTempFile,
copyPublicDir: vikeConfig.config.vite6BuilderApp
? // Already set by vike:build:pluginBuildApp
undefined
: !isViteServerBuild(config),
};
}
async function handleAssetsManifest(config, viteEnv, options, bundle) {
const isSsREnv = isViteServerBuild_onlySsrEnv(config, viteEnv);
if (isSsREnv) {
assert(!assetsJsonFilePath);
const outDirs = getOutDirs(config, viteEnv);
assetsJsonFilePath = path.posix.join(outDirs.outDirRoot, 'assets.json');
await writeAssetsManifestFile(outDirs, assetsJsonFilePath, config);
}
if (isViteServerBuild(config, viteEnv)) {
const outDir = options.dir;
assert(outDir);
// Replace __VITE_ASSETS_MANIFEST__ in server builds
// - Always replace it in dist/server/
// - Also in some other server builds such as dist/vercel/ from vike-vercel
// - Don't replace it in dist/rsc/ from vike-react-rsc since __VITE_ASSETS_MANIFEST__ doesn't exist there
const noop = await set_macro_ASSETS_MANIFEST(assetsJsonFilePath, bundle, outDir);
if (isSsREnv)
assert(!noop); // dist/server should always contain __VITE_ASSETS_MANIFEST__
}
}
async function writeAssetsManifestFile(outDirs, assetsJsonFilePath, config) {
const isFixEnabled = handleAssetsManifest_isFixEnabled(config);
const clientManifestFilePath = path.posix.join(outDirs.outDirClient, manifestTempFile);
const serverManifestFilePath = path.posix.join(outDirs.outDirServer, manifestTempFile);
if (!isFixEnabled) {
await fs.copyFile(clientManifestFilePath, assetsJsonFilePath);
}
else {
const { clientManifestMod } = await fixServerAssets(config);
await writeManifestFile(clientManifestMod, assetsJsonFilePath);
}
await fs.rm(clientManifestFilePath);
await fs.rm(serverManifestFilePath);
}