From 597c63b54b46aab2be66c9879ba2150b75e86ecd Mon Sep 17 00:00:00 2001 From: Devon Govett Date: Sun, 14 Jan 2024 18:38:53 -0500 Subject: [PATCH] WIP: add library bundler --- packages/bundlers/library/package.json | 26 ++++++ .../bundlers/library/src/LibraryBundler.js | 41 +++++++++ .../integration/sync-entry-shared/yarn.lock | 0 .../integration-tests/test/library-bundler.js | 92 +++++++++++++++++++ .../packagers/js/src/ScopeHoistingPackager.js | 14 ++- packages/runtimes/js/src/JSRuntime.js | 2 +- 6 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 packages/bundlers/library/package.json create mode 100644 packages/bundlers/library/src/LibraryBundler.js create mode 100644 packages/core/integration-tests/test/integration/sync-entry-shared/yarn.lock create mode 100644 packages/core/integration-tests/test/library-bundler.js diff --git a/packages/bundlers/library/package.json b/packages/bundlers/library/package.json new file mode 100644 index 000000000000..3498769305e3 --- /dev/null +++ b/packages/bundlers/library/package.json @@ -0,0 +1,26 @@ +{ + "name": "@parcel/bundler-library", + "version": "2.11.0", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "repository": { + "type": "git", + "url": "https://github.com/parcel-bundler/parcel.git" + }, + "main": "lib/LibraryBundler.js", + "source": "src/LibraryBundler.js", + "engines": { + "node": ">= 12.0.0", + "parcel": "^2.11.0" + }, + "dependencies": { + "@parcel/plugin": "2.11.0", + "nullthrows": "^1.1.1" + } +} diff --git a/packages/bundlers/library/src/LibraryBundler.js b/packages/bundlers/library/src/LibraryBundler.js new file mode 100644 index 000000000000..52c889f17ac5 --- /dev/null +++ b/packages/bundlers/library/src/LibraryBundler.js @@ -0,0 +1,41 @@ +// @flow strict-local +import {Bundler} from '@parcel/plugin'; +import nullthrows from 'nullthrows'; + +export default (new Bundler({ + bundle({bundleGraph}) { + let bundles = new Map(); + bundleGraph.traverse((node, context) => { + if (node.type === 'dependency') { + let dependency = node.value; + let parentAsset = bundleGraph.getAssetWithDependency(dependency); + let assets = bundleGraph.getDependencyAssets(dependency); + + // Create a separate bundle group/bundle for each asset. + for (let asset of assets) { + let target = nullthrows(dependency.target ?? context); + let bundleGroup = bundleGraph.createBundleGroup(dependency, target); + let bundle = bundleGraph.createBundle({ + entryAsset: asset, + needsStableName: !parentAsset, + target, + }); + bundleGraph.addAssetToBundle(asset, bundle); + bundleGraph.addBundleToBundleGroup(bundle, bundleGroup); + + // Reference the parent bundle so we create dependencies between them. + let parentBundle = parentAsset && bundles.get(parentAsset); + if (parentBundle) { + bundleGraph.createBundleReference(parentBundle, bundle); + } + bundles.set(asset, bundle); + } + + if (dependency.target) { + return dependency.target; + } + } + }); + }, + optimize() {}, +}): Bundler); diff --git a/packages/core/integration-tests/test/integration/sync-entry-shared/yarn.lock b/packages/core/integration-tests/test/integration/sync-entry-shared/yarn.lock new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/core/integration-tests/test/library-bundler.js b/packages/core/integration-tests/test/library-bundler.js new file mode 100644 index 000000000000..f729fb6a8c39 --- /dev/null +++ b/packages/core/integration-tests/test/library-bundler.js @@ -0,0 +1,92 @@ +// @flow strict-local +import assert from 'assert'; +import path from 'path'; +import { + bundle, + run, + overlayFS, + outputFS, + fsFixture, + assertBundles, +} from '@parcel/test-utils'; + +describe('library bundler', function () { + let count = 0; + let dir; + beforeEach(async () => { + dir = path.join(__dirname, 'libraries', '' + ++count); + await overlayFS.mkdirp(dir); + }); + + after(async () => { + await overlayFS.rimraf(path.join(__dirname, 'libraries')); + }); + + it('should support named imports', async function () { + await fsFixture(overlayFS, dir)` + yarn.lock: + + .parcelrc: + { + "extends": "@parcel/config-default", + "bundler": "@parcel/bundler-library" + } + + package.json: + { + "module": "dist/out.js" + } + + index.js: + export * from './foo'; + export * from './bar'; + + foo.js: + import {baz} from './baz'; + export function foo() { + return 'foo' + baz(); + } + + bar.js: + import {baz} from './baz'; + export function bar() { + return 'bar' + baz(); + } + + baz.js: + export function baz() { + return 'baz'; + } + `; + + let b = await bundle(path.join(dir, '/index.js'), { + inputFS: overlayFS, + mode: 'production', + }); + + let res = await run(b); + assert.equal(res.foo(), 'foobaz'); + assert.equal(res.bar(), 'barbaz'); + + assertBundles(b, [ + { + assets: ['index.js'], + }, + { + assets: ['foo.js'], + }, + { + assets: ['bar.js'], + }, + { + assets: ['baz.js'], + }, + ]); + + for (let bundle of b.getBundles()) { + let contents = await outputFS.readFile(bundle.filePath, 'utf8'); + assert(!contents.includes('parcelRequire')); + assert(contents.includes('export {')); + } + }); +}); diff --git a/packages/packagers/js/src/ScopeHoistingPackager.js b/packages/packagers/js/src/ScopeHoistingPackager.js index d12a2bac987d..532db9d41eca 100644 --- a/packages/packagers/js/src/ScopeHoistingPackager.js +++ b/packages/packagers/js/src/ScopeHoistingPackager.js @@ -137,10 +137,19 @@ export class ScopeHoistingPackager { for (let b of this.bundleGraph.getReferencedBundles(this.bundle)) { let entry = b.getMainEntry(); let symbols = new Map(); - if (entry && !this.isAsyncBundle && entry.type === 'js') { + if (entry && entry.type === 'js') { this.externalAssets.add(entry); let usedSymbols = this.bundleGraph.getUsedSymbols(entry) || new Set(); + let exportedSymbols = this.bundleGraph.getExportedSymbols(entry); + let hasNamespaceExport = exportedSymbols.some( + s => s.exportAs === '*', + ); + if (usedSymbols.has('*') && !hasNamespaceExport) { + // Use all exported symbols if a namespace is requested but does not exist. + usedSymbols = new Set(exportedSymbols.map(e => e.exportSymbol)); + } + for (let s of usedSymbols) { // If the referenced bundle is ESM, and we are importing '*', use 'default' instead. // This matches the logic below in buildExportedSymbols. @@ -324,7 +333,6 @@ export class ScopeHoistingPackager { if ( asset.meta.shouldWrap || - this.isAsyncBundle || this.bundle.env.sourceType === 'script' || this.bundleGraph.isAssetReferenced(this.bundle, asset) || this.bundleGraph @@ -361,7 +369,6 @@ export class ScopeHoistingPackager { buildExportedSymbols() { if ( - this.isAsyncBundle || !this.bundle.env.isLibrary || this.bundle.env.outputFormat !== 'esmodule' ) { @@ -1043,6 +1050,7 @@ ${code} .some( dep => !dep.isEntry && + this.bundle.hasDependency(dep) && nullthrows(this.bundleGraph.getUsedSymbols(dep)).has('*'), ))) || // If a symbol is imported (used) from a CJS asset but isn't listed in the symbols, diff --git a/packages/runtimes/js/src/JSRuntime.js b/packages/runtimes/js/src/JSRuntime.js index a083bafbd693..d66e1d6b5308 100644 --- a/packages/runtimes/js/src/JSRuntime.js +++ b/packages/runtimes/js/src/JSRuntime.js @@ -232,7 +232,7 @@ export default (new Runtime({ // Skip URL runtimes for library builds. This is handled in packaging so that // the url is inlined and statically analyzable. - if (bundle.env.isLibrary && dependency.meta?.placeholder != null) { + if (bundle.env.isLibrary && mainBundle.bundleBehavior !== 'isolated') { continue; }