Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: support legacy build with AST traversal #2

Closed
Menci opened this issue Feb 14, 2022 · 3 comments
Closed

proposal: support legacy build with AST traversal #2

Menci opened this issue Feb 14, 2022 · 3 comments

Comments

@Menci
Copy link

Menci commented Feb 14, 2022

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.

@chenxch
Copy link
Owner

chenxch commented Feb 15, 2022

The SystemJS mode was originally planned to be supported after researching @vitejs/plugin-legacy. Of course, I am looking forward to your PR.

@Menci
Copy link
Author

Menci commented Feb 15, 2022

I have a question. What does ${assetsMarker}*.*.* mean in:

const assetsMarker = `${base}${assetsDir}/`
const assetsMarkerRE = new RegExp(`("${assetsMarker}*.*.*")`, 'g')

@chenxch
Copy link
Owner

chenxch commented Feb 16, 2022

I have a question. What does ${assetsMarker}..* mean in:

const assetsMarker = `${base}${assetsDir}/`
const assetsMarkerRE = new RegExp(`("${assetsMarker}*.*.*")`, 'g')

This is when I read vite's implementation of assets path.
asset.ts
Now I feel that I don't need such detailed matching.

@chenxch chenxch closed this as completed Feb 23, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants