diff --git a/.eslintignore b/.eslintignore index 3189ad9a8..957e4a275 100644 --- a/.eslintignore +++ b/.eslintignore @@ -7,3 +7,7 @@ packages/typescript/test/fixtures/syntax-error # temporary workaround for eslint bug where package.json is a directory packages/node-resolve/test/fixtures/package-json-in-path + +# temporary workaround for TypeScript as it doesn't support "Arbitrary module namespace identifier names" +# https://github.com/microsoft/TypeScript/issues/40594 +packages/json/test/fixtures/arbitrary/main.js diff --git a/package.json b/package.json index f9b206d89..d8a9cff8d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint:js": "eslint --cache packages scripts shared util --ext .js,.ts,.mjs", "lint:json": "prettier --write .github/**/*.yml **/tsconfig.json tsconfig.*.json pnpm-workspace.yaml", "lint:package": "prettier --write **/package.json", - "package:release": "versioner --stripShortName='^@.+/plugin-' --target", + "package:release": "versioner --stripShortName='^@.+/(plugin-)?' --target", "preinstall": "node scripts/disallow-npm.js", "prepare": "husky install", "prettier": "prettier --write .", diff --git a/packages/alias/CHANGELOG.md b/packages/alias/CHANGELOG.md index 347f04921..f264d0276 100755 --- a/packages/alias/CHANGELOG.md +++ b/packages/alias/CHANGELOG.md @@ -1,5 +1,13 @@ # @rollup/plugin-alias ChangeLog +## v5.1.0 + +_2023-11-25_ + +### Features + +- feat: add warning to avoid unintended duplicate modules (#1634) + ## v5.0.1 _2023-10-05_ diff --git a/packages/alias/package.json b/packages/alias/package.json index 11449daaf..602d87eb9 100755 --- a/packages/alias/package.json +++ b/packages/alias/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/plugin-alias", - "version": "5.0.1", + "version": "5.1.0", "publishConfig": { "access": "public" }, diff --git a/packages/alias/src/index.ts b/packages/alias/src/index.ts index 8c5072571..51503848c 100755 --- a/packages/alias/src/index.ts +++ b/packages/alias/src/index.ts @@ -1,3 +1,5 @@ +import path from 'path'; + import type { Plugin } from 'rollup'; import type { ResolvedAlias, ResolverFunction, ResolverObject, RollupAliasOptions } from '../types'; @@ -97,7 +99,18 @@ export default function alias(options: RollupAliasOptions = {}): Plugin { updatedId, importer, Object.assign({ skipSelf: true }, resolveOptions) - ).then((resolved) => resolved || { id: updatedId }); + ).then((resolved) => { + if (resolved) return resolved; + + if (!path.isAbsolute(updatedId)) { + this.warn( + `rewrote ${importee} to ${updatedId} but was not an abolute path and was not handled by other plugins. ` + + `This will lead to duplicated modules for the same path. ` + + `To avoid duplicating modules, you should resolve to an absolute path.` + ); + } + return { id: updatedId }; + }); } }; } diff --git a/packages/alias/test/fixtures/folder/warn-importee.js b/packages/alias/test/fixtures/folder/warn-importee.js new file mode 100644 index 000000000..0f4e35a4a --- /dev/null +++ b/packages/alias/test/fixtures/folder/warn-importee.js @@ -0,0 +1 @@ +console.log() diff --git a/packages/alias/test/fixtures/warn.js b/packages/alias/test/fixtures/warn.js new file mode 100644 index 000000000..f655535b5 --- /dev/null +++ b/packages/alias/test/fixtures/warn.js @@ -0,0 +1,2 @@ +import '@/warn-importee.js' +import './folder/warn-importee.js' diff --git a/packages/alias/test/test.mjs b/packages/alias/test/test.mjs index 55a67aa0d..2f0be4eab 100755 --- a/packages/alias/test/test.mjs +++ b/packages/alias/test/test.mjs @@ -1,6 +1,7 @@ import path, { posix } from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; +import os from 'os'; import test from 'ava'; import { rollup } from 'rollup'; @@ -10,13 +11,16 @@ import alias from 'current-package'; const DIRNAME = fileURLToPath(new URL('.', import.meta.url)); +const isWindows = os.platform() === 'win32'; + /** * Helper function to test configuration with Rollup * @param plugins is an array of plugins for Rollup, they should include "alias" + * @param externalIds is an array of ids that will be external * @param tests is an array of pairs [source, importer] * @returns {Promise} */ -function resolveWithRollup(plugins, tests) { +function resolveWithRollup(plugins, externalIds, tests) { if (!plugins.find((p) => p.name === 'alias')) { throw new Error('`plugins` should include the alias plugin.'); } @@ -41,6 +45,7 @@ function resolveWithRollup(plugins, tests) { }, resolveId(id) { if (id === 'dummy-input') return id; + if (externalIds.includes(id)) return { id, external: true }; return null; }, load(id) { @@ -58,11 +63,12 @@ function resolveWithRollup(plugins, tests) { /** * Helper function to test configuration with Rollup and injected alias plugin * @param aliasOptions is a configuration for alias plugin + * @param externalIds is an array of ids that will be external * @param tests is an array of pairs [source, importer] * @returns {Promise} */ -function resolveAliasWithRollup(aliasOptions, tests) { - return resolveWithRollup([alias(aliasOptions)], tests); +function resolveAliasWithRollup(aliasOptions, externalIds, tests) { + return resolveWithRollup([alias(aliasOptions)], externalIds, tests); } test('type', (t) => { @@ -90,6 +96,7 @@ test('Simple aliasing (array)', (t) => { find: './local', replacement: 'global' } ] }, + ['bar', 'paradise', 'global'], [ { source: 'foo', importer: '/src/importer.js' }, { source: 'pony', importer: '/src/importer.js' }, @@ -106,6 +113,7 @@ test('Simple aliasing (object)', (t) => './local': 'global' } }, + ['bar', 'paradise', 'global'], [ { source: 'foo', importer: '/src/importer.js' }, { source: 'pony', importer: '/src/importer.js' }, @@ -125,6 +133,7 @@ test('RegExp aliasing', (t) => { find: /^test\/$/, replacement: 'this/is/strict' } ] }, + ['fooooooooobar2019', 'i/am/a/barbie/girl', 'this/is/strict'], [ { source: 'fooooooooobar', @@ -150,6 +159,7 @@ test('Will not confuse modules with similar names', (t) => { find: './foo', replacement: 'bar' } ] }, + [], [ { source: 'foo2', importer: '/src/importer.js' }, { @@ -168,6 +178,7 @@ test('Alias entry file', (t) => { entries: [{ find: 'abacaxi', replacement: './abacaxi' }] }, + ['./abacaxi/entry.js'], // eslint-disable-next-line no-undefined [{ source: 'abacaxi/entry.js' }] ).then((result) => t.deepEqual(result, ['./abacaxi/entry.js']))); @@ -177,11 +188,17 @@ test('i/am/a/file', (t) => { entries: [{ find: 'resolve', replacement: 'i/am/a/file' }] }, + ['i/am/a/file'], [{ source: 'resolve', importer: '/src/import.js' }] ).then((result) => t.deepEqual(result, ['i/am/a/file']))); -test('Windows absolute path aliasing', (t) => - resolveAliasWithRollup( +test('Windows absolute path aliasing', (t) => { + if (!isWindows) { + t.deepEqual(1, 1); + return null; + } + + return resolveAliasWithRollup( { entries: [ { @@ -190,13 +207,15 @@ test('Windows absolute path aliasing', (t) => } ] }, + [], [ { source: 'resolve', importer: posix.resolve(DIRNAME, './fixtures/index.js') } ] - ).then((result) => t.deepEqual(result, ['E:\\react\\node_modules\\fbjs\\lib\\warning']))); + ).then((result) => t.deepEqual(result, ['E:\\react\\node_modules\\fbjs\\lib\\warning'])); +}); /** * Helper function to get moduleIDs from final Rollup bundle @@ -276,6 +295,7 @@ test('Global customResolver function', (t) => { ], customResolver: () => customResult }, + [], [ { source: 'test', @@ -300,6 +320,7 @@ test('Local customResolver function', (t) => { ], customResolver: () => customResult }, + [], [ { source: 'test', @@ -322,6 +343,7 @@ test('Global customResolver plugin-like object', (t) => { ], customResolver: { resolveId: () => customResult } }, + [], [ { source: 'test', @@ -348,6 +370,7 @@ test('Local customResolver plugin-like object', (t) => { ], customResolver: { resolveId: () => customResult } }, + [], [ { source: 'test', @@ -373,6 +396,7 @@ test('supports node-resolve as a custom resolver', (t) => ], customResolver: nodeResolvePlugin() }, + [], [ { source: 'local-resolver', @@ -450,6 +474,7 @@ test('Forwards isEntry and custom options to a custom resolver', (t) => { return args[0]; } }, + [], [ { source: 'entry', importer: '/src/importer.js', options: { isEntry: true } }, { @@ -497,9 +522,15 @@ test('Forwards isEntry and custom options to other plugins', (t) => { name: 'test', resolveId(...args) { resolverCalls.push(args); + + if (['entry-point', 'non-entry-point'].includes(args[0])) { + return { id: args[0], external: true }; + } + return null; } } ], + [], [ { source: 'entry', importer: '/src/importer.js', options: { isEntry: true } }, { @@ -558,6 +589,7 @@ test('CustomResolver plugin-like object with buildStart', (t) => { buildStart: () => (count.option += 1) } }, + [], [] ).then(() => t.deepEqual(count, { @@ -608,3 +640,34 @@ test('Works as CJS plugin', async (t) => { ) ); }); + +test('show warning for non-absolute non-plugin resolved id', async (t) => { + const warnList = []; + await rollup({ + input: './test/fixtures/warn.js', + plugins: [ + alias({ + entries: [ + { + find: '@', + replacement: path.relative(process.cwd(), path.join(DIRNAME, './fixtures/folder')) + } + ] + }) + ], + onwarn(log) { + const formattedLog = { ...log, message: log.message.replace(/\\/g, '/') }; + warnList.push(formattedLog); + } + }); + t.deepEqual(warnList, [ + { + message: + 'rewrote @/warn-importee.js to test/fixtures/folder/warn-importee.js but was not an abolute path and was not handled by other plugins. ' + + 'This will lead to duplicated modules for the same path. ' + + 'To avoid duplicating modules, you should resolve to an absolute path.', + code: 'PLUGIN_WARNING', + plugin: 'alias' + } + ]); +}); diff --git a/packages/dynamic-import-vars/CHANGELOG.md b/packages/dynamic-import-vars/CHANGELOG.md index 2be9ff627..4c6e08a44 100644 --- a/packages/dynamic-import-vars/CHANGELOG.md +++ b/packages/dynamic-import-vars/CHANGELOG.md @@ -1,5 +1,21 @@ # @rollup/plugin-dynamic-import-vars ChangeLog +## v2.1.2 + +_2023-11-28_ + +### Bugfixes + +- fix: Allow a "no files found" error to be emitted as warning (#1625) + +## v2.1.1 + +_2023-11-25_ + +### Bugfixes + +- fix: escape special glob characters (#1636) + ## v2.1.0 _2023-10-25_ diff --git a/packages/dynamic-import-vars/README.md b/packages/dynamic-import-vars/README.md index bd3eb82bd..ae663cdf2 100644 --- a/packages/dynamic-import-vars/README.md +++ b/packages/dynamic-import-vars/README.md @@ -68,6 +68,8 @@ Default: `false` By default, the plugin will not throw errors when target files are not found. Setting this option to true will result in errors thrown when encountering files which don't exist. +⚠️ _Important:_ Enabling this option when `warnOnError` is set to `true` will result in a warning and _not_ an error + #### `warnOnError` Type: `Boolean`
diff --git a/packages/dynamic-import-vars/package.json b/packages/dynamic-import-vars/package.json index 2a6358341..3674a5955 100644 --- a/packages/dynamic-import-vars/package.json +++ b/packages/dynamic-import-vars/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/plugin-dynamic-import-vars", - "version": "2.1.0", + "version": "2.1.2", "publishConfig": { "access": "public" }, diff --git a/packages/dynamic-import-vars/src/dynamic-import-to-glob.js b/packages/dynamic-import-vars/src/dynamic-import-to-glob.js index 4d3f30b3f..6dcf7d7fd 100644 --- a/packages/dynamic-import-vars/src/dynamic-import-to-glob.js +++ b/packages/dynamic-import-vars/src/dynamic-import-to-glob.js @@ -1,15 +1,18 @@ import path from 'path'; +import fastGlob from 'fast-glob'; + export class VariableDynamicImportError extends Error {} /* eslint-disable-next-line no-template-curly-in-string */ const example = 'For example: import(`./foo/${bar}.js`).'; function sanitizeString(str) { + if (str === '') return str; if (str.includes('*')) { throw new VariableDynamicImportError('A dynamic import cannot contain * characters.'); } - return str; + return fastGlob.escapePath(str); } function templateLiteralToGlob(node) { diff --git a/packages/dynamic-import-vars/src/index.js b/packages/dynamic-import-vars/src/index.js index 3d631b729..b44770c45 100644 --- a/packages/dynamic-import-vars/src/index.js +++ b/packages/dynamic-import-vars/src/index.js @@ -56,11 +56,14 @@ function dynamicImportVariables({ include, exclude, warnOnError, errorWhenNoFile ); if (errorWhenNoFilesFound && paths.length === 0) { - this.error( - new Error( - `No files found in ${glob} when trying to dynamically load concatted string from ${id}` - ) + const error = new Error( + `No files found in ${glob} when trying to dynamically load concatted string from ${id}` ); + if (warnOnError) { + this.warn(error); + } else { + this.error(error); + } } // create magic string if it wasn't created already diff --git a/packages/dynamic-import-vars/test/rollup-plugin-dynamic-import-vars.test.js b/packages/dynamic-import-vars/test/rollup-plugin-dynamic-import-vars.test.js index 6a0ae9448..58c40091c 100644 --- a/packages/dynamic-import-vars/test/rollup-plugin-dynamic-import-vars.test.js +++ b/packages/dynamic-import-vars/test/rollup-plugin-dynamic-import-vars.test.js @@ -218,7 +218,7 @@ test("doesn't throw if no files in dir when option isn't set", async (t) => { t.false(thrown); }); -test('throws if no files in dir when option is set', async (t) => { +test('throws if no files in dir when `errorWhenNoFilesFound` is set', async (t) => { let thrown = false; try { await rollup({ @@ -236,3 +236,21 @@ test('throws if no files in dir when option is set', async (t) => { } t.true(thrown); }); + +test('warns if no files in dir when `errorWhenNoFilesFound` and `warnOnError` are both set', async (t) => { + let warningEmitted = false; + await rollup({ + input: 'fixture-no-files.js', + plugins: [dynamicImportVars({ errorWhenNoFilesFound: true, warnOnError: true })], + onwarn(warning) { + t.deepEqual( + warning.message, + `No files found in ./module-dir-c/*.js when trying to dynamically load concatted string from ${require.resolve( + './fixtures/fixture-no-files.js' + )}` + ); + warningEmitted = true; + } + }); + t.true(warningEmitted); +}); diff --git a/packages/dynamic-import-vars/test/src/dynamic-import-to-glob.test.mjs b/packages/dynamic-import-vars/test/src/dynamic-import-to-glob.test.mjs index a08624b49..17f50124c 100644 --- a/packages/dynamic-import-vars/test/src/dynamic-import-to-glob.test.mjs +++ b/packages/dynamic-import-vars/test/src/dynamic-import-to-glob.test.mjs @@ -268,3 +268,21 @@ test('throws when dynamic import imports does not contain a file extension', (t) ); t.true(error instanceof VariableDynamicImportError); }); + +test('escapes ()', (t) => { + const ast = parse('import(`./${foo}/(foo).js`);', { + sourceType: 'module' + }); + + const glob = dynamicImportToGlob(ast.body[0].expression.source); + t.is(glob, './*/\\(foo\\).js'); +}); + +test('escapes []', (t) => { + const ast = parse('import(`./${foo}/[foo].js`);', { + sourceType: 'module' + }); + + const glob = dynamicImportToGlob(ast.body[0].expression.source); + t.is(glob, './*/\\[foo\\].js'); +}); diff --git a/packages/esm-shim/CHANGELOG.md b/packages/esm-shim/CHANGELOG.md index ca747cac8..247719d1a 100644 --- a/packages/esm-shim/CHANGELOG.md +++ b/packages/esm-shim/CHANGELOG.md @@ -1,5 +1,13 @@ # @rollup/plugin-esm-shim ChangeLog +## v0.1.6 + +_2024-04-05_ + +### Bugfixes + +- fix: do not insert shims at `import` literal (#1696) + ## v0.1.5 _2023-11-05_ diff --git a/packages/esm-shim/package.json b/packages/esm-shim/package.json index a1089a3f9..aa9d53d21 100644 --- a/packages/esm-shim/package.json +++ b/packages/esm-shim/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/plugin-esm-shim", - "version": "0.1.5", + "version": "0.1.6", "publishConfig": { "access": "public" }, diff --git a/packages/esm-shim/src/constants.ts b/packages/esm-shim/src/constants.ts index ef2953f5e..9f00e960c 100644 --- a/packages/esm-shim/src/constants.ts +++ b/packages/esm-shim/src/constants.ts @@ -11,4 +11,4 @@ const require = cjsModule.createRequire(import.meta.url); `; export const ESMStaticImportRegex = - /(?<=\s|^|;)import\s*([\s"']*(?[\w\t\n\r $*,/{}]+)from\s*)?["']\s*(?(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][\s;]*/gm; + /(?<=\s|^|;)import\s*([\s"']*(?[\w\t\n\r $*,/{}]+)from\s*)?["']\s*(?(?<="\s*)[^"\n]*[^\s"](?=\s*")|(?<='\s*)[^'\n]*[^\s'](?=\s*'))\s*["'][\s;]*/gm; diff --git a/packages/esm-shim/test/fixtures/cjs-import-literal.js b/packages/esm-shim/test/fixtures/cjs-import-literal.js new file mode 100644 index 000000000..915b1ceaf --- /dev/null +++ b/packages/esm-shim/test/fixtures/cjs-import-literal.js @@ -0,0 +1,6 @@ +const dn = __dirname; + +module.exports = { + keyword: ' import', + dn +}; diff --git a/packages/esm-shim/test/snapshots/test.js.md b/packages/esm-shim/test/snapshots/test.js.md index 1447dcc3a..98c82442e 100644 --- a/packages/esm-shim/test/snapshots/test.js.md +++ b/packages/esm-shim/test/snapshots/test.js.md @@ -93,3 +93,23 @@ Generated by [AVA](https://avajs.dev). ␊ export { c, child, s };␊ ` + +## inject cjs shim should not break on valid js object with `import` literal value + +> Snapshot 1 + + `␊ + // -- Shims --␊ + import cjsUrl from 'node:url';␊ + import cjsPath from 'node:path';␊ + import cjsModule from 'node:module';␊ + const __filename = cjsUrl.fileURLToPath(import.meta.url);␊ + const __dirname = cjsPath.dirname(__filename);␊ + const require = cjsModule.createRequire(import.meta.url);␊ + const dn = __dirname;␊ + ␊ + module.exports = {␊ + keyword: ' import',␊ + dn␊ + };␊ + ` diff --git a/packages/esm-shim/test/snapshots/test.js.snap b/packages/esm-shim/test/snapshots/test.js.snap index 7c8a3ca3d..56705e652 100644 Binary files a/packages/esm-shim/test/snapshots/test.js.snap and b/packages/esm-shim/test/snapshots/test.js.snap differ diff --git a/packages/esm-shim/test/test.js b/packages/esm-shim/test/test.js index 3f9e5f8e4..24f5a3291 100644 --- a/packages/esm-shim/test/test.js +++ b/packages/esm-shim/test/test.js @@ -67,3 +67,19 @@ test.serial('inject cjs shim for esm output with multiple import statements', as t.snapshot(output.code); t.falsy(output.map); }); + +// see issue #1649 https://github.com/rollup/plugins/issues/1649 +test.serial( + 'inject cjs shim should not break on valid js object with `import` literal value', + async (t) => { + const bundle = await rollup({ + input: 'test/fixtures/cjs-import-literal.js', + plugins: [esmShim()] + }); + const result = await bundle.generate({ format: 'es' }); + t.is(result.output.length, 1); + const [output] = result.output; + t.snapshot(output.code); + t.falsy(output.map); + } +); diff --git a/packages/inject/src/index.js b/packages/inject/src/index.js index 6077b5475..9030c8a1d 100644 --- a/packages/inject/src/index.js +++ b/packages/inject/src/index.js @@ -96,7 +96,7 @@ export default function inject(options) { } catch (err) { this.warn({ code: 'PARSE_ERROR', - message: `rollup-plugin-inject: failed to parse ${id}. Consider restricting the plugin to particular files via options.include` + message: `@rollup/plugin-inject: failed to parse ${id}. Consider restricting the plugin to particular files via options.include` }); } if (!ast) { diff --git a/packages/json/CHANGELOG.md b/packages/json/CHANGELOG.md index 0f412eb44..62553b080 100755 --- a/packages/json/CHANGELOG.md +++ b/packages/json/CHANGELOG.md @@ -1,5 +1,13 @@ # @rollup/plugin-json ChangeLog +## v6.1.0 + +_2023-12-12_ + +### Features + +- feat: add `includeArbitraryNames` option (#1641) + ## v6.0.1 _2023-10-05_ diff --git a/packages/json/README.md b/packages/json/README.md index e98f81fe5..1b1c9cd0f 100644 --- a/packages/json/README.md +++ b/packages/json/README.md @@ -75,6 +75,13 @@ Default: `null` A [picomatch pattern](https://github.com/micromatch/picomatch), or array of patterns, which specifies the files in the build the plugin should operate on. By default all files are targeted. +### `includeArbitraryNames` + +Type: `Boolean`
+Default: `false` + +If `true` and `namedExports` is `true`, generates a named export for not a valid identifier properties of the JSON object by leveraging the ["Arbitrary Module Namespace Identifier Names" feature](https://github.com/tc39/ecma262/pull/2154). + ### `indent` Type: `String`
diff --git a/packages/json/package.json b/packages/json/package.json index 883b4960b..007f14be0 100755 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/plugin-json", - "version": "6.0.1", + "version": "6.1.0", "publishConfig": { "access": "public" }, @@ -60,7 +60,7 @@ } }, "dependencies": { - "@rollup/pluginutils": "^5.0.1" + "@rollup/pluginutils": "^5.1.0" }, "devDependencies": { "@rollup/plugin-buble": "^1.0.0", diff --git a/packages/json/src/index.js b/packages/json/src/index.js index b61ed2300..dcb8f3333 100755 --- a/packages/json/src/index.js +++ b/packages/json/src/index.js @@ -18,6 +18,7 @@ export default function json(options = {}) { preferConst: options.preferConst, compact: options.compact, namedExports: options.namedExports, + includeArbitraryNames: options.includeArbitraryNames, indent }), map: { mappings: '' } diff --git a/packages/json/test/fixtures/arbitrary/foo.json b/packages/json/test/fixtures/arbitrary/foo.json new file mode 100755 index 000000000..0776ea877 --- /dev/null +++ b/packages/json/test/fixtures/arbitrary/foo.json @@ -0,0 +1,3 @@ +{ + "foo.bar": "baz" +} diff --git a/packages/json/test/fixtures/arbitrary/main.js b/packages/json/test/fixtures/arbitrary/main.js new file mode 100755 index 000000000..75670fae1 --- /dev/null +++ b/packages/json/test/fixtures/arbitrary/main.js @@ -0,0 +1,3 @@ +export { 'foo.bar' as bar } from './foo.json'; + +result = exports; // eslint-disable-line no-undef diff --git a/packages/json/test/test.js b/packages/json/test/test.js index 1abbc2377..9aec69089 100755 --- a/packages/json/test/test.js +++ b/packages/json/test/test.js @@ -54,6 +54,17 @@ test('generates named exports', async (t) => { t.is(code.indexOf('this-should-be-excluded'), -1, 'should exclude unused properties'); }); +test('generates named exports including arbitrary names', async (t) => { + const bundle = await rollup({ + input: 'fixtures/arbitrary/main.js', + plugins: [json({ includeArbitraryNames: true })] + }); + + const { result } = await testBundle(t, bundle, { inject: { exports: {} } }); + + t.is(result.bar, 'baz'); +}); + test('resolves extensionless imports in conjunction with the node-resolve plugin', async (t) => { const bundle = await rollup({ input: 'fixtures/extensionless/main.js', diff --git a/packages/pluginutils/CHANGELOG.md b/packages/pluginutils/CHANGELOG.md index f1af3f625..ae8bd877d 100755 --- a/packages/pluginutils/CHANGELOG.md +++ b/packages/pluginutils/CHANGELOG.md @@ -1,5 +1,17 @@ # @rollup/pluginutils ChangeLog +## v5.1.0 + +_2023-11-28_ + +### Features + +- feat: add `includeArbitraryNames: true` to `dataToEsm` (#1635) + +### Updates + +- doc: correct the return type of createFilter (#1633) + ## v5.0.5 _2023-10-05_ diff --git a/packages/pluginutils/README.md b/packages/pluginutils/README.md index e705cb912..942e4d4c8 100755 --- a/packages/pluginutils/README.md +++ b/packages/pluginutils/README.md @@ -64,7 +64,7 @@ Attaches `Scope` objects to the relevant nodes of an AST. Each `Scope` object ha Parameters: `(ast: Node, propertyName?: String)`
Returns: `Object` -See [rollup-plugin-inject](https://github.com/rollup/rollup-plugin-inject) or [rollup-plugin-commonjs](https://github.com/rollup/rollup-plugin-commonjs) for an example of usage. +See [@rollup/plugin-inject](https://github.com/rollup/plugins/tree/master/packages/inject) or [@rollup/plugin-commonjs](https://github.com/rollup/plugins/tree/master/packages/commonjs) for an example of usage. ```js import { attachScopes } from '@rollup/pluginutils'; @@ -100,7 +100,7 @@ export default function myPlugin(options = {}) { Constructs a filter function which can be used to determine whether or not certain modules should be operated upon. Parameters: `(include?: , exclude?: , options?: Object)`
-Returns: `String` +Returns: `(id: string | unknown) => boolean` #### `include` and `exclude` @@ -143,7 +143,7 @@ export default function myPlugin(options = {}) { Transforms objects into tree-shakable ES Module imports. -Parameters: `(data: Object)`
+Parameters: `(data: Object, options: DataToEsmOptions)`
Returns: `String` #### `data` @@ -152,6 +152,12 @@ Type: `Object` An object to transform into an ES module. +#### `options` + +Type: `DataToEsmOptions` + +_Note: Please see the TypeScript definition for complete documentation of these options_ + #### Usage ```js @@ -167,7 +173,8 @@ const esModuleSource = dataToEsm( indent: '\t', preferConst: true, objectShorthand: true, - namedExports: true + namedExports: true, + includeArbitraryNames: false } ); /* diff --git a/packages/pluginutils/package.json b/packages/pluginutils/package.json index 1351bcb2c..7cb1b9f0c 100644 --- a/packages/pluginutils/package.json +++ b/packages/pluginutils/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/pluginutils", - "version": "5.0.5", + "version": "5.1.0", "publishConfig": { "access": "public" }, diff --git a/packages/pluginutils/src/dataToEsm.ts b/packages/pluginutils/src/dataToEsm.ts index d51d59e1a..312017220 100755 --- a/packages/pluginutils/src/dataToEsm.ts +++ b/packages/pluginutils/src/dataToEsm.ts @@ -59,6 +59,17 @@ function serialize(obj: unknown, indent: Indent, baseIndent: string): string { return stringify(obj); } +// isWellFormed exists from Node.js 20 +const hasStringIsWellFormed = 'isWellFormed' in String.prototype; + +function isWellFormedString(input: string): boolean { + // @ts-expect-error String::isWellFormed exists from ES2024. tsconfig lib is set to ES6 + if (hasStringIsWellFormed) return input.isWellFormed(); + + // https://github.com/tc39/proposal-is-usv-string/blob/main/README.md#algorithm + return !/\p{Surrogate}/u.test(input); +} + const dataToEsm: DataToEsm = function dataToEsm(data, options = {}) { const t = options.compact ? '' : 'indent' in options ? options.indent : '\t'; const _ = options.compact ? '' : ' '; @@ -78,8 +89,19 @@ const dataToEsm: DataToEsm = function dataToEsm(data, options = {}) { return `export default${magic}${code};`; } + let maxUnderbarPrefixLength = 0; + for (const key of Object.keys(data)) { + const underbarPrefixLength = key.match(/^(_+)/)?.[0].length ?? 0; + if (underbarPrefixLength > maxUnderbarPrefixLength) { + maxUnderbarPrefixLength = underbarPrefixLength; + } + } + + const arbitraryNamePrefix = `${'_'.repeat(maxUnderbarPrefixLength + 1)}arbitrary`; + let namedExportCode = ''; const defaultExportRows = []; + const arbitraryNameExportRows: string[] = []; for (const [key, value] of Object.entries(data)) { if (key === makeLegalIdentifier(key)) { if (options.objectShorthand) defaultExportRows.push(key); @@ -93,11 +115,27 @@ const dataToEsm: DataToEsm = function dataToEsm(data, options = {}) { defaultExportRows.push( `${stringify(key)}:${_}${serialize(value, options.compact ? null : t, '')}` ); + if (options.includeArbitraryNames && isWellFormedString(key)) { + const variableName = `${arbitraryNamePrefix}${arbitraryNameExportRows.length}`; + namedExportCode += `${declarationType} ${variableName}${_}=${_}${serialize( + value, + options.compact ? null : t, + '' + )};${n}`; + arbitraryNameExportRows.push(`${variableName} as ${JSON.stringify(key)}`); + } } } - return `${namedExportCode}export default${_}{${n}${t}${defaultExportRows.join( + + const arbitraryExportCode = + arbitraryNameExportRows.length > 0 + ? `export${_}{${n}${t}${arbitraryNameExportRows.join(`,${n}${t}`)}${n}};${n}` + : ''; + const defaultExportCode = `export default${_}{${n}${t}${defaultExportRows.join( `,${n}${t}` )}${n}};${n}`; + + return `${namedExportCode}${arbitraryExportCode}${defaultExportCode}`; }; export { dataToEsm as default }; diff --git a/packages/pluginutils/test/dataToEsm.ts b/packages/pluginutils/test/dataToEsm.ts index d1d5d9ac5..d32fe3e24 100755 --- a/packages/pluginutils/test/dataToEsm.ts +++ b/packages/pluginutils/test/dataToEsm.ts @@ -110,3 +110,20 @@ test('avoid U+2029 U+2029 -0 be ignored by JSON.stringify, and avoid it return n 'export default[-0,"\\u2028\\u2029",undefined,undefined];' ); }); + +test('support arbitrary module namespace identifier names', (t) => { + t.is( + dataToEsm( + { foo: 'foo', 'foo.bar': 'foo.bar', '\udfff': 'non wellformed' }, + { namedExports: true, includeArbitraryNames: true } + ), + 'export var foo = "foo";\nvar _arbitrary0 = "foo.bar";\nexport {\n\t_arbitrary0 as "foo.bar"\n};\nexport default {\n\tfoo: foo,\n\t"foo.bar": "foo.bar",\n\t"\\udfff": "non wellformed"\n};\n' + ); + t.is( + dataToEsm( + { foo: 'foo', 'foo.bar': 'foo.bar', '\udfff': 'non wellformed' }, + { namedExports: true, includeArbitraryNames: true, compact: true } + ), + 'export var foo="foo";var _arbitrary0="foo.bar";export{_arbitrary0 as "foo.bar"};export default{foo:foo,"foo.bar":"foo.bar","\\udfff":"non wellformed"};' + ); +}); diff --git a/packages/pluginutils/types/index.d.ts b/packages/pluginutils/types/index.d.ts index fb96d6346..4bdf3a2a6 100755 --- a/packages/pluginutils/types/index.d.ts +++ b/packages/pluginutils/types/index.d.ts @@ -10,6 +10,12 @@ export interface AttachedScope { export interface DataToEsmOptions { compact?: boolean; + /** + * @desc When this option is set, dataToEsm will generate a named export for keys that + * are not a valid identifier, by leveraging the "Arbitrary Module Namespace Identifier + * Names" feature. See: https://github.com/tc39/ecma262/pull/2154 + */ + includeArbitraryNames?: boolean; indent?: string; namedExports?: boolean; objectShorthand?: boolean; diff --git a/packages/typescript/CHANGELOG.md b/packages/typescript/CHANGELOG.md index 9ec69af85..2e8fae7d4 100644 --- a/packages/typescript/CHANGELOG.md +++ b/packages/typescript/CHANGELOG.md @@ -1,5 +1,13 @@ # @rollup/plugin-typescript ChangeLog +## v11.1.6 + +_2024-01-09_ + +### Bugfixes + +- fix: Ensure rollup 4 compatibility (#1658) + ## v11.1.5 _2023-10-05_ diff --git a/packages/typescript/README.md b/packages/typescript/README.md index dce144b1f..8350f9aa8 100644 --- a/packages/typescript/README.md +++ b/packages/typescript/README.md @@ -63,7 +63,7 @@ export default { } ``` -The following options are unique to `rollup-plugin-typescript`: +The following options are unique to `@rollup/plugin-typescript`: ### `exclude` diff --git a/packages/typescript/package.json b/packages/typescript/package.json index fa3750a02..b83e7ca8b 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@rollup/plugin-typescript", - "version": "11.1.5", + "version": "11.1.6", "publishConfig": { "access": "public" }, @@ -63,7 +63,7 @@ } }, "dependencies": { - "@rollup/pluginutils": "^5.0.1", + "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "devDependencies": { diff --git a/packages/virtual/package.json b/packages/virtual/package.json index 115a8acf4..873493a29 100755 --- a/packages/virtual/package.json +++ b/packages/virtual/package.json @@ -12,7 +12,7 @@ }, "author": "Rich Harris", "homepage": "https://github.com/rollup/plugins/tree/master/packages/virtual#readme", - "bugs": "https://github.com/rollup/rollup-plugin-virtual/issues", + "bugs": "https://github.com/rollup/plugins/issues", "main": "./dist/cjs/index.js", "module": "./dist/es/index.js", "exports": { diff --git a/packages/virtual/src/index.ts b/packages/virtual/src/index.ts index 511b5bfad..29049a9c0 100644 --- a/packages/virtual/src/index.ts +++ b/packages/virtual/src/index.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import type { Plugin } from 'rollup'; -import type { RollupVirtualOptions } from '../'; +import type { RollupVirtualOptions } from '../types'; const PREFIX = `\0virtual:`; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbe85b834..3f5074977 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -468,8 +468,8 @@ importers: packages/json: dependencies: '@rollup/pluginutils': - specifier: ^5.0.1 - version: 5.0.1(rollup@4.0.0-24) + specifier: ^5.1.0 + version: 5.1.0(rollup@4.0.0-24) devDependencies: '@rollup/plugin-buble': specifier: ^1.0.0 @@ -728,8 +728,8 @@ importers: packages/typescript: dependencies: '@rollup/pluginutils': - specifier: ^5.0.1 - version: 5.0.1(rollup@4.0.0-24) + specifier: ^5.1.0 + version: 5.1.0(rollup@4.0.0-24) resolve: specifier: ^1.22.1 version: 1.22.1 @@ -2425,6 +2425,21 @@ packages: rollup: 4.0.0-24 dev: false + /@rollup/pluginutils@5.1.0(rollup@4.0.0-24): + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.0 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 4.0.0-24 + dev: false + /@rollup/rollup-android-arm-eabi@4.0.0-24: resolution: {integrity: sha512-19cF3V1fHfzPzwu0cgZEdWLMdNkqSmKOhidqQv1CkUqAMcb7etA7WLx8YrX5ob31ruI0BYYrUDBunlIuMHHUrg==} cpu: [arm]