lwc-prebundle v0.0.2
lwc-prebundle
Use external dependencies from node_modules
in your lwc component bundles without having to deal with static resources, lost typings, having to bundle them yourself, or cluttering up your lwc
folder in your sfdx projects.
Upgrade dependencies as you would in any off-platform project.
Let yarn/npm still take care of your lock files.
Runs on CI machines as long as they can run node and allow for fs
operations.
If Salesforce ever does provide a way to use code from node_modules
(unlikely, although a few TC-39 propsals might help) within your LWC components you can just not call lwc-prebundle before deploying with no need to change your code at all.
As i'm sure goes without saying, NOT RECOMMENDED FOR PRODUCTION.
Overview
Before you deploy your code to Salesforce lwc-prebundle will scan all the .js
files in your lwc/
sub-folders for imports to node_modules
and redirect them to an LWC component with the same name. That LWC component simply imports the actual third party code into a utility file, that is ignored by Salesforce, and re-exports everything that's imported. When the code is uploaded to Salesforce the only code that is imported or exported is from one LWC to another.
Once your deployment is finished, lwc-prebundle resets your import statements back to the original node_modules
location so that you get all the typings, etc that the original package provides.
lwc-prebundle will put each package into it's own LWC bundle, as opposed to a singular LWC bundle for all dependencies, for a few reasons:
- Each LWC bundle can only be 128kb
- Avoid namespace collisions between packages as everything needs to be re-exported through a singular js file named the same as the component
- Improved stacktraces that represent the actual external package name
- Ensures that no dependency code from another component in the same project has to be parsed for any component that doesn't use that dependency. This component should be treeshaken by lwc when it builds that particular component.
A common concern with providing a unique LWC bundle for each external dependency is that it will clutter up the lwc/
folder in your sfdx project. To mitigate this lwc-prebundle only stores the external dependency LWC bundle within the lwc
folder until the deployment is complete and hides it away when not in use. This also allows for treating this external code the same way you would in any other project, not part of your authored code.
lwc-prebundle keeps track of the specific imports you've used from the dependencies and will only re-bundle the dependency if somewhere in your code you add or remove an import from that dependency. Deploying to Salesforce already takes long enough, wherever possible we try not to increase the bundling time.
Right now the cleanup step is optional if you want to perform a one-time import.
Usage
- Call
lwc-bundle init
to add the proper globs to your.gitignore
and.forceignore
files. - Call
lwc-bundle prepare
whenever you push or deploy to Salesforce - Call
lwc-bundle cleanup
after your deployment is complete
It's easiest to add your scripts to package.json
like so:
"prepare": "lwc-prebundle prepare",
"cleanup": "lwc-prebundle cleanup",
"push": "lwc-prebundle prepare && sfdx force:source:push && lwc-prebundle cleanup",
If you need to pass flags to the sfdx push
or sfdx deploy
commands you can string them together yourself:
yarn prepare && sfdx force:source:push -FLAGS && yarn cleanup
https://stackoverflow.com/questions/50835221/pass-command-line-argument-to-child-script-in-yarn
TODO
- If a dependency no longer exists delete the cmp folder of the cache so it doesn't take up disk space unnecessarily
- Make available as SFDX plugin to take advantage of predeploy and postdeploy hooks
- This is only going to be worth doing if the predeploy hook gets called if nothing in the source seems to have changed and if the postdeploy hooks get called if the deployment fails, which I don't think that it will, so.
- Bypass the trash when deleting the cached files if possible?
Roadmap
Typescript Support
Add prompts on init
to select if looking for Typescript for non-component files. We will still only pass .ts files to rollup.
We can offer expirimental support for component files using the comment approach but it is very much experimental.
We will look in the root of the lwc folder for a tsconfig file to use, falling back to the root if it doesn't exist there.
It's interesting to consider ts support for non-cmp files because how much code actually gets written outside of cmp files? I mean it would be nice to be able to have all utilities written with types, etc. You could also get in the habit of writing most of your code in the non-cmp file but that adds indirection so I don't know.
Terser
Using the rollup terser plugin is easy once the config file is available. This would be your way to decrease the likelihood that a particular module will exceed the limit. It might be a waste of time though because lwc is going to minify your code anyway so not doing this saves build time. They don't minify before saving, just before serving, so do this if you need to get under the limit and this will do that, but don't waste build time otherwise.
Babel Support
Babel support can be setup through the config file if desired.
The reason I would offer TypeScript support first-class and not babel is because when Salesforce does it's own bundling I'm pretty sure it uses babel and rollup as well so the only real reason you would need babel is to get like top-level await or something (although optional chaining still doesn't seem to be available by default in lwc)
Plus there's no way to ship sourcemaps but I guess with like an esnext target of babel it would look a lot like your code.
Post-CSS Support
Ability to run post-css on cmp {css|scss} files
support if desired needs to be introduced as first class, if it were to exist, as right now we only deal with the .js
files and ignore the .html
and .css
files. An option would be to scan for css and scss files, if there's a configuration for one.
I'm not entirely sure how useful this feature would be generally though. Being able to use sass would be nice but css is already scoped and can be shared with @import
statements. You also don't end up with much css in a given component if you use slds. It would be interesting to have a css service module and just import it into every component but because of the trash that is closed shadow roots and the way lwc scopes css it would be insanely expensive to do that, so.
I would love to be able to support Tailwind for a particular component (I know slds exists but I don't like the provided utilities). However, because of the lack of any global styles it's really not something that should be done because even if you were willing to run purgecss each and every time per component for the entire nearly 4MB tailwind dev build you would run into the same cost with bundling a unique scoped version of each utility class per component.
Config file support
Will support a lwc-prebundle.config.{js|json} with keys for every file that you want to bundle. All external dependencies are bundled by default but this offers an opportunity to inject rollup configuration for a particular dependency. No non-external code is bundled unless the component bundle name is listed in the configuration file.
This will basically support anything rollup supports, with the exceptions being you can't overwrite certain required configuration such as the output being set to type esm
.
All the sub files of a component with this setup need to have the same configuration which I guess isn't the end of the world and I also imagine that you really just want to set the configuration for all files so you can use something like typescript (or terser or post-css with emit until we add it)
Will need a way to pass a flag to the CLI, or to set it here, so that if you want to run some version of the configuration for everything, with the specific ones falling back to that (or being merged with that).
You would get a mapping like a.js
-> a.bundle.js
within the same component but because we can't run rollup on the cmp js file itself we'll need to modify those imports also which isn't a big deal because it's the same as what we're already doing and rollup obviously preserves exports.
We could just do it the same way we do the external stuff by adding the .bundle.js
file only while uploading but we would literally just delete it and rebundle because we're not going to reinvent rollups caching and start hashing the actual contents of a file or something. Would be pretty cool to be able to look into the .git folder the same way vscode does to see if the file is modified?
import typescript from '@rollup/plugin-typescript'
export default {
// external dependency
robot3: {
include: '*', // optional, unlikely to provide for an external dep but this shows everything,
},
// authored component
myComponent: {
include: ['utils.ts'], // need to provide extension per, could offer string|string[]
input: {
plugins: [
typescript,
/* plugin function references, do not call them */
],
},
output: {
file: ['utils.js'], // this is a nice way to do it if you write ts so no need
},
},
}
LWC Single File Components
Basically identical to vue components just an html file that supports a top level template tag, style tag, and script tag.
Just need to set your editor to treat .lwc files as html files to get syntax highlighting. Will need to see how eslint handles this, if they check the script section of html files in the project, probably not so that's worth considering.
Alternative Foldering
Having to have all components at the same level of the singular lwc/
folder starts to get really unwieldly very quickly (yes you could split them out into multiple packages but code organization is absolutely not the reason to do that).
It would be great to be able to have a src/
folder and nest the components to 2 depths so you can organize them better at least.
src
feature-1
cmp-a
cmp-b
cmp-c
Known Issues & Limitations
- You cannot have an LWC component named the same as an external dependency
- If your deployment fails then the script afterward doesn't run...but this monstrosity is a current workaround:
lwc-prebundle prepare && (sfdx force:source:push || lwc-prebundle cleanup) && lwc-prebundle cleanup
- If you have a string literal in a code file that matches exactly with the name of an imported module, such as
let str = "lodash/fp"
, you will experience issues with that being replaced withlet str = "c/lodash/fp"
- There's the chance that an imported package name will break the rules of what lwc folders can be named
- lwc-prebundle can handle path based imports such as
lodash/fp
but there are other restrictions that are not handled
- lwc-prebundle can handle path based imports such as
- There is known size limitation of 128KB for each treeshaken external module that gets included
- A common concern surrounds adopting an external module and exceeding the size limit only as you begin to use additional pieces of the package.
- BundlePhobia is your friend when evaluating a dependency
Codebase
- Consider migrating to reghex to parse the imports and their items
- Evaluate if snowpack might be better here than rollup
Notes
You cannot accomplish this in any way that requires actually parsing the component js files (such as rollup plugin alias, babel, etc) because you will need to transform the decorators,which means Salesforce won't accept the generated code.
Usage
$ npm install -g lwc-prebundle
$ lwc-prebundle COMMAND
running command...
$ lwc-prebundle (-v|--version|version)
lwc-prebundle/0.0.2 darwin-x64 node-v15.1.0
$ lwc-prebundle --help [COMMAND]
USAGE
$ lwc-prebundle COMMAND
...
Commands
lwc-prebundle cleanup
describe the command here
USAGE
$ lwc-prebundle cleanup
OPTIONS
-h, --help show CLI help
-r, --root=root the path from the project root to the lwc directory
See code: src/commands/cleanup.ts
lwc-prebundle help [COMMAND]
display help for lwc-prebundle
USAGE
$ lwc-prebundle help [COMMAND]
ARGUMENTS
COMMAND command to show help for
OPTIONS
--all see all commands in CLI
See code: @oclif/plugin-help
lwc-prebundle init
Configure your project for usage with lwc-prebundle
USAGE
$ lwc-prebundle init
OPTIONS
-h, --help show CLI help
See code: src/commands/init.ts
lwc-prebundle prepare
describe the command here
USAGE
$ lwc-prebundle prepare
OPTIONS
-h, --help show CLI help
-r, --root=root the path from the project root the lwc directory
See code: src/commands/prepare.ts