vike
Version:
(Replaces Next.js/Nuxt) 🔨 Composable framework to build advanced applications with flexibility and stability.
326 lines (325 loc) • 12.9 kB
JavaScript
export { pluginDistFileNames };
// Attempt to preserve file structure of `.page.js` files:
// - https://github.com/vikejs/vike/commit/11a4c49e5403aa7c37c8020c462b499425b41854
// - Blocker: https://github.com/rollup/rollup/issues/4724
import { assert, assertUsage } from '../../../../utils/assert.js';
import { isArray } from '../../../../utils/isArray.js';
import { isCallable } from '../../../../utils/isCallable.js';
import { assertPosixPath } from '../../../../utils/path.js';
import path from 'node:path';
import crypto from 'node:crypto';
import { getAssetsDir } from '../../shared/getAssetsDir.js';
import { assertModuleId, getFilePathToShowToUserModule } from '../../shared/getFilePath.js';
import '../../assertEnvVite.js';
import { isVersionMatch } from '../../../../utils/assertVersion.js';
function pluginDistFileNames() {
return [
{
name: 'vike:build:pluginDistFileNames',
apply: 'build',
enforce: 'post',
configResolved: {
handler(config) {
const rollupOutputs = getRollupOutputs(config);
// We need to support multiple outputs: @vite/plugin-legacy adds an output, see https://github.com/vikejs/vike/issues/477#issuecomment-1406434802
rollupOutputs.forEach((rollupOutput) => {
if (!('entryFileNames' in rollupOutput)) {
rollupOutput.entryFileNames = (chunkInfo) => getEntryFileName(chunkInfo, config, true);
}
if (!('chunkFileNames' in rollupOutput)) {
rollupOutput.chunkFileNames = (chunkInfo) => getChunkFileName(chunkInfo, config);
}
if (!('assetFileNames' in rollupOutput)) {
rollupOutput.assetFileNames = (chunkInfo) => getAssetFileName(chunkInfo, config);
rollupOutput.assetFileNames.isTheOneSetByVike = true;
assert(rollupOutput.assetFileNames.isTheOneSetByVike);
}
else {
// If a user needs this:
// - assertUsage() that the naming provided by the user ends with `.[hash][extname]`
// - It's needed for getHash() of handleAssetsManifest()
// - Asset URLs should always contain a hash: it's paramount for caching assets.
// - If rollupOutput.assetFileNames is a function then use a wrapper function to apply the assertUsage()
assertUsage(rollupOutput.assetFileNames.isTheOneSetByVike, "Setting Vite's configuration build.rollupOptions.output.assetFileNames is currently forbidden. Reach out if you need to use it.");
}
});
disableCSSBundling(config);
},
},
},
];
}
function getIdHash(id) {
return crypto.createHash('md5').update(id).digest('hex').slice(0, 8);
}
function getAssetFileName(assetInfo, config) {
const userRootDir = config.root;
const assetsDir = getAssetsDir(config);
const dir = assetsDir + '/static';
let { name } = assetInfo;
if (!name) {
return `${dir}/[name].[hash][extname]`;
}
// https://github.com/vikejs/vike/issues/794
assertPosixPath(name);
name = path.posix.basename(name);
// dist/client/assets/index.page.server.jsx_extractAssets_lang.e4e33422.css
// => dist/client/assets/index.page.server.e4e33422.css
if (
// Vite 2
name?.endsWith('_extractAssets_lang.css') ||
// Vite 3
name?.endsWith('?extractAssets&lang.css')) {
name = name.split('.').slice(0, -2).join('.');
name = clean(name, userRootDir);
return `${dir}/${name}.[hash][extname]`;
}
name = name.split('.').slice(0, -1).join('.');
name = clean(name, userRootDir);
return `${dir}/${name}.[hash][extname]`;
}
function getChunkFileName(_chunkInfo, config) {
const isForClientSide = !config.build.ssr;
let name = 'chunks/chunk-[hash].js';
if (isForClientSide) {
const assetsDir = getAssetsDir(config);
name = `${assetsDir}/${name}`;
}
return name;
}
function getEntryFileName(chunkInfo, config, isEntry) {
const userRootDir = config.root;
const assetsDir = getAssetsDir(config);
const isForClientSide = !config.build.ssr;
let { name } = chunkInfo;
assertPosixPath(name);
name = clean(name, userRootDir, true,
// Not needed for client-side because dist/ filenames contain `.[hash].js`
!isForClientSide);
if (isForClientSide) {
return `${assetsDir}/${name}.[hash].js`;
}
else {
return `${name}.${isEntry ? 'mjs' : 'js'}`;
}
}
function removePathSeparators(name) {
assertPosixPath(name);
assert(!name.startsWith('/'), { name });
const entryDir = 'entries/';
const hasEntryDir = name.startsWith(entryDir);
if (hasEntryDir) {
name = name.slice(entryDir.length);
assert(!name.startsWith('/'));
}
name = name.split('/').join('_');
if (hasEntryDir) {
name = `${entryDir}${name}`;
}
return name;
}
function removeUserRootDir(name, userRootDir) {
if (name.startsWith(userRootDir)) {
name = name.slice(userRootDir.length);
if (name.startsWith('/'))
name = name.slice(1);
}
assert(!name.startsWith('/'), { name });
return name;
}
function clean(name, userRootDir, removePathSep, fixGlob) {
name = removeUserRootDir(name, userRootDir);
name = fixExtractAssetsQuery(name);
if (fixGlob) {
name = workaroundGlob(name);
}
name = replaceNonLatinCharacters(name);
if (removePathSep) {
name = removePathSeparators(name);
}
name = removeLeadingUnderscoreInFilename(name);
name = removeUnderscoreDoublets(name);
// Avoid:
// ```
// dist/client/assets/entries/.Dp9wM6PK.js
// dist/server/entries/.mjs
// ```
assert(!name.endsWith('/'));
return name;
}
function fixExtractAssetsQuery(name) {
name = name.replace(/\.[^\.]*_extractAssets_lang$/, '.extractAssets');
return name;
}
function removeUnderscoreDoublets(name) {
name = name.split(/__+/).join('_');
return name;
}
function replaceNonLatinCharacters(name) {
name = name.split('+').join('');
name = name.replace(/[^a-zA-Z0-9\/\._]/g, '-');
return name;
}
// Remove leading `_` from filename
// - GitHub Pages treat URLs with filename starting with `_` differently (removing the need for workaround of creating a .jekyll file)
function removeLeadingUnderscoreInFilename(name) {
assertPosixPath(name);
const paths = name.split('/');
{
const last = paths.length - 1;
let filename = paths[last];
if (filename.startsWith('_')) {
filename = filename.slice(1);
paths[last] = filename;
name = paths.join('/');
}
}
return name;
}
// Ensure import.meta.glob() doesn't match dist/ files
function workaroundGlob(name) {
// V1 design
name = name.split('+').join('');
['client', 'server', 'route'].forEach((env) => {
name = name.split(`.page.${env}`).join(`-page-${env}`);
});
name = name.split('.page.').join('-page.');
name = name.replace(/\.page$/, '-page');
return name;
}
// Workaround for Vite CSS duplication bug: https://github.com/vikejs/vike/issues/1815
function disableCSSBundling(config) {
if (isVite8OrAbove(config)) {
for (const output of getRolldownOutputs(config)) {
assert(output);
const { codeSplitting } = output;
// `codeSplitting: false` => single-bundle mode; respect the user's choice.
if (codeSplitting === false)
continue;
// `codeSplitting` set as an object => Rolldown ignores `manualChunks`, so inject a group into `codeSplitting.groups` instead.
if (codeSplitting && typeof codeSplitting === 'object') {
if (codeSplittingHasWorkaround.has(codeSplitting))
continue;
codeSplittingHasWorkaround.add(codeSplitting);
codeSplitting.groups ?? (codeSplitting.groups = []);
codeSplitting.groups.push({
test: /\.css$/,
name: (moduleId) => getCssChunkName(moduleId, config) ?? null,
/*
// Default priority — user-defined groups with the same priority appear earlier in the array and win the match.
// https://rolldown.rs/options/output-advanced-chunks
priority: 0
*/
});
continue;
}
// `codeSplitting` unset / `true` => wrap `manualChunks` (Rolldown auto-converts it to a group).
// - Rolldown supports `manualChunks` whenever `codeSplitting` isn't an object (despite what the migration guide implies).
wrapManualChunks(output, config, 'rolldownOptions');
}
}
else {
for (const output of getRollupOutputs(config)) {
assert(output);
wrapManualChunks(output, config, 'rollupOptions');
}
}
}
const codeSplittingHasWorkaround = new WeakSet();
function wrapManualChunks(output, config, optsName) {
const manualChunksOriginal = output.manualChunks;
// Sometimes applied twice => skip if we already wrapped — same rationale as `isTheOneSetByVike` for assetFileNames above.
if (manualChunksOriginal?.isTheOneSetByVike)
return;
output.manualChunks = function (id, ...args) {
if (manualChunksOriginal) {
if (isCallable(manualChunksOriginal)) {
const result = manualChunksOriginal.call(this, id,
// @ts-ignore
...args);
if (result !== undefined)
return result;
}
else {
assertUsage(false, `The Vite's configuration build.${optsName}.output.manualChunks must be a function. Reach out if you need to set it to another value.`);
}
}
return getCssChunkName(id, config);
};
output.manualChunks.isTheOneSetByVike = true;
}
function getCssChunkName(id, config) {
if (!id.endsWith('.css'))
return undefined;
const userRootDir = config.root;
if (id.startsWith(userRootDir)) {
assertPosixPath(id);
assertModuleId(id);
let name;
const isNodeModules = id.match(/node_modules\/([^\/]+)\/(?!.*node_modules)/);
if (isNodeModules) {
name = isNodeModules[1];
}
else {
const filePath = getFilePathToShowToUserModule(id, config);
name = filePath;
name = name.split('.').slice(0, -1).join('.'); // remove file extension
name = name.split('/').filter(Boolean).join('_');
}
// Make fileHash the same between local development and CI
const idStable = path.posix.relative(userRootDir, id);
// Don't remove `?` queries because each `id` should belong to a unique bundle.
const hash = getIdHash(idStable);
return `${name}-${hash}`;
}
else {
let name;
const isVirtualModule = id.match(/virtual:([^:]+):/);
if (isVirtualModule) {
name = isVirtualModule[1];
assert(name);
}
else if (
// https://github.com/vikejs/vike/issues/1818#issuecomment-2298478321
id.startsWith('/__uno')) {
name = 'uno';
}
else {
name = 'style';
}
const hash = getIdHash(id);
return `${name}-${hash}`;
}
}
function isVite8OrAbove(config) {
const viteVersion = config._viteVersionResolved;
assert(viteVersion);
return isVersionMatch(viteVersion, ['8.0.0']);
}
function getRollupOutputs(config) {
var _a, _b;
// @ts-expect-error is read-only
config.build ?? (config.build = {});
(_a = config.build).rollupOptions ?? (_a.rollupOptions = {});
(_b = config.build.rollupOptions).output ?? (_b.output = {});
const { output } = config.build.rollupOptions;
if (!isArray(output)) {
return [output];
}
return output;
}
function getRolldownOutputs(config) {
var _a, _b;
// @ts-expect-error is read-only
config.build ?? (config.build = {});
// @ts-ignore
(_a = config.build).rolldownOptions ?? (_a.rolldownOptions = {});
// @ts-ignore
(_b = config.build.rolldownOptions).output ?? (_b.output = {});
// @ts-ignore
const { output } = config.build.rolldownOptions;
if (!isArray(output)) {
return [output];
}
return output;
}