parcel-bundler / parcel

The zero configuration build tool for the web. πŸ“¦πŸš€

Home Page:https://parceljs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Creating dynamic aliases.

tcmulder opened this issue Β· comments

πŸ™‹ feature request

Is it possible to create a dynamic alias? I'm using Parcel 2 for WordPress Gutenberg block development, so I need something that handles matching multiple imports to external resources. For example, imports for @wordpress/blocks and @wordpress/block-editor need to alias the global wp object as wp.blocks and wp.blockEditor.

πŸ€” Expected Behavior

I'm hoping something like the following could work in the package.json file. It takes all @wordpress/something-kebab-case imports and converts them to the format wp.sometingCamelCase (or wp[somethingCamelCase]):

{
    "name": "wp-parcel",
    "version": "1.0.0",
    "description": "Parcel build tool for WordPress",
    "scripts": "...",
    "alias": {
        "@wordpress/*": {
            "global": "wp[$1.toLowerCase().replace(/(-\w)/g, m => m.toUpperCase().substr(1))]"
        }
    },
    "devDependencies": {
        "parcel": "^2.9.3"
    }
}

😯 Current Behavior

Currently, I need to list each and every import separately. The following example shows just shows two, and it does work in this format, but there are dozens more (and WordPress could add even more in the future), so it really bloats my package.json to cover each import separately like this:

{
    "name": "wp-parcel",
    "version": "1.0.0",
    "description": "Parcel build tool for WordPress",
    "scripts": "...",
    "alias": {
        "@wordpress/blocks": {
            "global": "wp.blocks"
        },
        "@wordpress/block-editor": {
            "global": "wp.blockEditor"
        },
        ...
    },
    "devDependencies": {
        "parcel": "^2.9.3"
    }
}

πŸ’ Possible Solution

Glob aliases are similar to what I'm looking for, but they don't allow me to do some processing like transforming from kebab case to camel case, and even "global": "wp[$1]" chokes with a $1 undefined message.

This plugin may have worked for Parcel 1, but it looks like there are no immediate plans to make it work for Parcel 2. If Parcel 2 natively supported an external file that could handle the transformation like that older plugin does, it could be a good solution to reduce the size of the package.json.

πŸ”¦ Context

I'm loving Parcel 2 for WordPress development and have been simply destructuring the global wp object instead of importing dependencies:

// rather than
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
// I instad do
const { InspectorControls, useBlockProps } = wp.blockEditor;

But, all WordPress examples and tutorials online follow the import syntax rather than destructuring, so as I'm sharing my custom theme with other developers I'm getting some raised eyebrows, and it's becoming a hassle to convert imports to destructured objects any time I copy code from the WordPress docs. If Parcel 2 supported dynamic aliases, it'd allow projects like mine to better match the coding standards of the platforms for which we're building.

Try writing a custom Resolver plugin: https://parceljs.org/plugin-system/resolver/

Thanks @devongovett! I'm not very familiar with building my own resolvers: any chance you could point me in the right direction?

I created a basic resolver that simply logs the package and object names I want to match, then does nothing else:

import {Resolver} from '@parcel/plugin';

export default new Resolver({
  async resolve(data) {
    const {specifier} = data;
    if (specifier.startsWith('@wordpress')) {
		const propertyName = `${specifier}`.substring(11).toLowerCase().replace(/(-\w)/g, m => m.toUpperCase().substring(1));
		console.log({
			importNameIs: specifier,
			objectNameShouldBe: `wp[${propertyName}]`
		})
    }
    return null;
  }
});

I see examples at the link you provided for changing the filePath a package should use, but no pointers on ignoring the package altogether and using a globally-available object.property instead. Any help would be appreciated!

on ignoring the package altogether

https://parceljs.org/plugin-system/resolver/#excluding-modules

using a globally-available object.property instead

https://parceljs.org/plugin-system/resolver/#virtual-modules
with code being module.exports = object.property;

Thanks @mischnic ! Possibly some progress. When I use the following, parcel watch no longer throws any errors in the console:

import {Resolver} from '@parcel/plugin';

export default new Resolver({
  async resolve(data) {
    const {specifier} = data;
    if (specifier.startsWith('@wordpress')) {
		const propertyName = `${specifier}`.substring(11).toLowerCase().replace(/(-\w)/g, m => m.toUpperCase().substring(1));
		return {
			isExcluded: true,
			code: `module.exports = wp[${propertyName}];`
		};
    }
    return null;
  }
});

However, in the browser I'm getting errors like "cannot find module '@wordpress/blocks'", and on parcel build I'm getting the error "external modules are not supported when building for browser."

Is isExcluded not taking effect for some reason?

Just remove the isExcluded: true. Then it should do what you want.

That did the trick: thanks so much for your help!

Actually, that didn't work: turns out I still had my hard-coded aliases in the package.json file. Once I remove those, it's still erroring in the same manner as before. What can I try next?

Turns out you do need to return a filepath as well. So that imports contained in the returned code can be resolved and so that Parcel knows the filetype of the returned code.

So you need

const { Resolver } = require("@parcel/plugin");
const path = require("path");

module.exports = new Resolver({
  async resolve({ options, specifier }) {
    console.log(specifier);
    if (specifier.startsWith("@wordpress")) {
      const propertyName = specifier
        .substring(11)
        .toLowerCase()
        .replace(/(-\w)/g, (m) => m.toUpperCase().substring(1));
      return {
        filePath: path.join(options.projectRoot, `wp-${propertyName}.js`), // this is arbitrary, ideally unique, has to have a JS extension
        code: `module.exports = wp[${propertyName}];`,
      };
    }
    return null;
  },
});

Parcel should throw an error in this case and tell you this

Hmmm, still not working πŸ™

Screenshot 2024-03-22 at 10 50 21β€―AM

Here's a full example that's working for me: https://github.com/mischnic/parcel-issue-9588

Thanks so much, that did do the trick!

I needed to fix some code I had sent you to include quotes for the object property: it was resolving to wp[blockEditor] and throwing a runtime error of "undefined variable blockEditor", rather than resolving to wp['blockEditor']which is correct (i.e. the same aswp.blockEditor`). That didn't affect the build though, and is now fixed. Below is the code that worked for me in case it's helpful to someone else:

In the .parcelrc file:

{
  "extends": [
    "@parcel/config-default"
  ],
  "resolvers": [
    ".parcel-resolvers.mjs",
    "..."
  ]
}

In the .parcel-resolvers.mjs file (or whatever you choose to name it):

import {Resolver} from '@parcel/plugin';
import path from 'path';

export default new Resolver({
  async resolve({ options, specifier }) {
    if (specifier.startsWith('@wordpress') && ! ['@wordpress/icons'].includes(specifier)) {
      const propertyName = specifier
        .substring(11)
        .toLowerCase()
        .replace(/(-\w)/g, (m) => m.toUpperCase().substring(1));
      return {
        filePath: path.join(
          options.projectRoot,
          `wp-${propertyName}.js`
        ),
        code: `module.exports = wp['${propertyName}'];`,
      };
    }
    return null;
  },
});

Thanks again!