The current implementation of this plugin does only support the modern ESM target and not the legacy target (SystemJS). The challenge of supporting legacy target is that legacy build not only includes base
as the prefix of importing module path, but also bundles CSS data containing absolute url()
references in JS chunks, which is not easy to replace by regex.
My solution is doing it by AST traversal. SWC is a blazing fast library to process JS code. First use a unique placeholder for base
like /__vite_base__/
. Then we can just find all string literals and replace them with expressions like "string1" + window.publicPath + "string2" + window.publicPath + "string3"
.
Here is my locally patched version of this plugin, which implements this feature:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.dynamicBase = void 0;
/// @ts-check
const swc = require("@swc/core");
const { Visitor } = require("@swc/core/Visitor.js");
class PlaceholderReplacer extends Visitor {
constructor(placeholder, expression) {
super();
this.placeholder = placeholder;
this.expression = PlaceholderReplacer.parseExpression(expression);
}
static parseExpression(expression) {
return swc.parseSync(expression).body[0].expression;
}
/**
* @param {import("@swc/core").StringLiteral} node
* @returns {import("@swc/core").Expression}
*/
visitStringLiteral(node) {
const stringParts = node.value.split(this.placeholder);
if (stringParts.length === 1) return node;
const createStringExpression = (str) => ({
type: 'StringLiteral',
span: { start: 0, end: 0, ctxt: 0 },
value: str,
hasEscape: true,
kind: { type: 'normal', containsQuote: true }
});
let subExpressions = Array(stringParts.length * 2 - 1);
for (let i = 0; i < stringParts.length; i++) {
subExpressions[i * 2] = createStringExpression(stringParts[i]);
if (i !== stringParts.length - 1) {
subExpressions[i * 2 + 1] = this.expression;
}
}
subExpressions = subExpressions.filter(expr => expr.type !== "StringLiteral" || expr.value !== "");
if (subExpressions.length === 1) return subExpressions[0];
const createAddExpression = (left) => ({
type: 'BinaryExpression',
span: { start: 0, end: 0, ctxt: 0 },
operator: '+',
left: left,
right: null
});
let rootExpression;
let previousExpression;
for (let i = 0; i < subExpressions.length; i++) {
const currentSubExpression = subExpressions[i];
if (i === 0) {
previousExpression = rootExpression = createAddExpression(currentSubExpression);
} else if (i === subExpressions.length - 1) {
previousExpression.right = currentSubExpression;
} else {
previousExpression.right = createAddExpression(currentSubExpression);
previousExpression = previousExpression.right;
}
}
return rootExpression;
}
}
function dynamicBase(options) {
const { publicPath = 'window.__dynamic_base__' } = options || {};
let assetsDir = 'assets';
let base = '/';
return {
name: 'vite-plugin-dynamic-base',
enforce: 'post',
apply: 'build',
configResolved(resolvedConfig) {
assetsDir = resolvedConfig.build.assetsDir;
base = resolvedConfig.base;
},
async generateBundle({ format }, bundle) {
if (format !== 'es' && format !== 'system') {
return;
}
const assetsRE = new RegExp(`${base}${assetsDir}/`, 'g');
await Promise.all(Object.entries(bundle).map(async ([, chunk]) => {
if (chunk.type === 'chunk' && chunk.code.indexOf(base) > -1) {
const ast = await swc.parse(chunk.code);
const replacer = new PlaceholderReplacer(base, publicPath);
replacer.visitModule(ast);
chunk.code = (await swc.print(ast, { minify: true })).code;
} else if (chunk.type === 'asset' && chunk.fileName.endsWith(".css")) {
// Emitted CSS files, for modern build, just let them use relative paths
chunk.source = chunk.source.replace(assetsRE, "");
}
}));
}
};
}
exports.dynamicBase = dynamicBase;
You can check the result on my website. I could create a PR If you consider this feature useful and accepts my solution.