wmr: Userland plugin doesn't work if it uses the the same file extension as a built-in plugin, with a different import prefix

UPDATE: the bugs still exist, but I now have found solutions to work around WMR’s handling of core file extensions, such as SVG and JSON files. Here is a working text: plugin that does not require using alternative file extensions in order to avoid colliding with WMR’s own handling (e.g. *.jsonld for JSON or *.xml for SVG):

/** @type {import('wmr').Plugin} */
function textPlugin() {
    const IMPORT_PREFIX = 'text:';
    const INTERNAL_PREFIX = `\0${IMPORT_PREFIX}`;

    return {
        name: 'text-plugin',

        async resolveId(id, importer) {
            if (id[0] === '\0' || id[0] === '\b') return;

            if (id.startsWith(IMPORT_PREFIX)) {
                id = id.slice(IMPORT_PREFIX.length);
            } else return;

            // necessary for SVG file extension
            // (otherwise WMR's url: import prefix plugin creates an inline data: URL for the SVG asset)
            if (config.mode === 'start') {
                id = `${id}${'\0'}`;
            }

            // just in case the given importer has an import prefix
            if (importer) {
                importer = importer.replace(/^[\0\b]\w+:/g, '');
            }

            const resolved = await this.resolve(id, importer, { skipSelf: true });
            if (!resolved) {
                return;
            }

            // necessary for JSON file extension
            // (because WMR's json: import prefix plugin handles it by default)
            if (/^[\0\b]\w+:/g.test(resolved.id)) {
                resolved.id = resolved.id.replace(/^[\0\b]\w+:/g, '');
            }

            return `${INTERNAL_PREFIX}${resolved.id}`;
        },

        async load(id) {
            if (!id.startsWith(INTERNAL_PREFIX)) return;

            id = id.slice(INTERNAL_PREFIX.length);

            // necessary for SVG file extension
            // (otherwise WMR's url: import prefix plugin creates an inline data: URL for the SVG asset)
            if (config.mode === 'start') {
                id = id.replace(/\0$/, '');
            }

            id = path.resolve(config.cwd || '.', id);

            this.addWatchFile(id);

            const s = fs.readFileSync(id, { encoding: 'utf8' });

            return `export default \`${s}\``;
        },
    };
}
textPlugin.enforce = 'pre';
config.plugins.push(textPlugin());

Describe the bug

WMR’s built-in plugins that handle specific file extensions (e.g. SVG in the “URL plugin”) interfere with userland plugins that wish to handle the same file extensions under a different scheme (i.e. custom import prefix, e.g. svg: to include the raw SVG markup instead of creating an external image URL)

Culprit:

https://github.com/preactjs/wmr/blob/97eed9d79558fd8bd9baa1a4503a7bea781b7a5a/packages/wmr/src/plugins/url-plugin.js#L4

To Reproduce

cake.svg =>

<!-- https://heroicons.com/ -->
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
	stroke-linecap="round"
	stroke-linejoin="round"
	stroke-width="2"
	d="M21 15.546c-.523 0-1.046.151-1.5.454a2.704 2.704 0 01-3 0 2.704 2.704 0 00-3 0 2.704 2.704 0 01-3 0 2.704 2.704 0 00-3 0 2.704 2.704 0 01-3 0 2.701 2.701 0 00-1.5-.454M9 6v2m3-2v2m3-2v2M9 3h.01M12 3h.01M15 3h.01M21 21v-7a2 2 0 00-2-2H5a2 2 0 00-2 2v7h18zm-3-9v-2a2 2 0 00-2-2H8a2 2 0 00-2 2v2h12z"
/>
</svg>
  1. npm init wmr svg-import
  2. cd svg-import
  3. open public/pages/_404.js, add at the top import svgCakeUrl from './cake.svg' (which is equivalent to import svgCakeUrl from 'url:./cake.svg'), and add in the JSX <img src={svgCakeUrl} alt="" width={48} height={48} />
  4. npm start (or npm run build --prerender && npm run serve) => all works fine, but the image asset remains external, now we want to inline the SVG markup inside the JSX. Next step:
  5. Add at the top import svgCakeContent from 'svg:./cake.svg', and insert the following userland plugin in wmr.config.mjs:

UPDATE: scroll to the top of this thread to see the most up to date, working text: plugin.

function svgPlugin() {
    const IMPORT_PREFIX = 'svg:';
    const INTERNAL_PREFIX = '\0svg:';
    const SVGEXT = '.svg';
    return {
        name: 'svg-plugin',
        async resolveId(id, importer) {
            if (id[0] === '\0' || id[0] === '\b') return;

            if (id.startsWith(IMPORT_PREFIX)) {
                id = id.slice(IMPORT_PREFIX.length);
            } else if (config.mode === 'build') {
                return;
            } else if (!id.endsWith(SVGEXT)) {
                return;
            }

            const resolved = await this.resolve(id, importer, { skipSelf: true });
            if (!resolved) return;

            resolved.id = `${INTERNAL_PREFIX}${resolved.id}`;
            return resolved.id;
        },
        async load(id) {
            if (!id.startsWith(INTERNAL_PREFIX)) return;

            id = id.slice(INTERNAL_PREFIX.length);

            id = path.resolve(config.cwd || '.', id);
            this.addWatchFile(id);

            const s = fs.readFileSync(id, { encoding: 'utf8' });
            console.log(j);

            return `export default \`${s}\``;
        },
        transform(code, id) {

            if (!id.endsWith(SVGEXT) || id.startsWith(INTERNAL_PREFIX)) return;

            return {
                code: `export default \`${code}\``,
                map: null,
            };
        },
    };
}
config.plugins.push(svgPlugin());
  1. Now run npm start (or npm run build --prerender && npm run serve), and refresh the page => fail.
  2. Copy the file cake.svg to cake.svgx (for example), change the extension accordingly in the above plugin (const SVGEXT = '.svgx') and of course in the import as well (import svgCakeContent from 'svg:./cake.svgx') => success.

I added a “trace” plugin to console.log() the resolveId() etc., and indeed there seems to be some interference due to the common SVG file extension. Related issue: https://github.com/preactjs/wmr/issues/446#issuecomment-802225547

Expected behavior

WMR’s built-in plugins should allow userland plugins to define custom import prefixes for common file extensions.

Desktop (please complete the following information):

  • OS: all
  • Browser: all
  • WMR Version: all

Additional context

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 20 (5 by maintainers)

Most upvoted comments

Ah, great! I found a trick to work around the JSON file extension (i.e. to bypass WMR’s core json: import prefix handler):

After the this.resolve() call in my text: plugin I now “sanitize” the returned resolve.id, ensuring no other import prefix was somewhat injected by WMR’s core plugin chain:

if (/^[\0\b]\w+:/g.test(resolved.id)) {
    resolved.id = resolved.id.replace(/^[\0\b]\w+:/g, '');
}

I’ll update my text: plugin code snippet accordingly, in previous comments in this thread.

PS: just in case the importer itself carries an import prefix, I also sanitize it before calling this.resolve(id, importer, { skipSelf: true }):

if (importer) {
    importer = importer.replace(/^[\0\b]\w+:/g, '');
}

My goal is that plugins (including internal ones) share the same order in prod and dev.

Well done for cutting a new release! 👍 (which I am now using instead of a self-build from the main branch)

I updated my post in this thread about the ordered list of plugins: https://github.com/preactjs/wmr/issues/449#issuecomment-808466358

By the way, a built-in text: import prefix plugin would be nice as a core WMR feature, don’t you think? 😃 That being said, I think it is more important that userland plugins can handle common file extensions without colliding with WMR’s own internal file handling / import logic.