cli: add module support for rollup build

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-12-24 12:35:52 +01:00
parent 47407fc34c
commit e1d50e2ce5
8 changed files with 180 additions and 13 deletions
+2
View File
@@ -72,6 +72,8 @@ export interface BackstagePackageJson {
[key: string]: string;
};
// (undocumented)
type?: 'module' | 'commonjs';
// (undocumented)
types?: string;
// (undocumented)
typesVersions?: Record<string, Record<string, string[]>>;
@@ -43,6 +43,8 @@ export interface BackstagePackageJson {
// that the package bundles all of its dependencies in its build output.
bundled?: boolean;
type?: 'module' | 'commonjs';
backstage?: {
role?: PackageRole;
moved?: string;
+91 -8
View File
@@ -16,7 +16,11 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { relative as relativePath, resolve as resolvePath } from 'path';
import {
extname,
relative as relativePath,
resolve as resolvePath,
} from 'path';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import postcss from 'rollup-plugin-postcss';
@@ -29,6 +33,7 @@ import {
RollupOptions,
OutputOptions,
WarningHandlerWithDefault,
OutputPlugin,
} from 'rollup';
import { forwardFileImports } from './plugins';
@@ -40,6 +45,11 @@ import { readEntryPoints } from '../entryPoints';
const SCRIPT_EXTS = ['.js', '.jsx', '.ts', '.tsx'];
const MODULE_EXTS = ['.mjs', '.mts'];
const COMMONJS_EXTS = ['.cjs', '.cts'];
const MOD_EXT = '.mjs';
const CJS_EXT = '.cjs';
function isFileImport(source: string) {
if (source.startsWith('.')) {
return true;
@@ -68,6 +78,39 @@ function buildInternalImportPattern(options: BuildOptions) {
return new RegExp(`^(?:${names.join('|')})(?:$|/)`);
}
// This Rollup output plugin enables support for mixed CommonJS and ESM output.
// It does it be filtering out the unwanted output files that don't match the
// input file format, allowing the rollup configuration to have overlapping
// output configurations for different formats.
function multiOutputFormat(): OutputPlugin {
return {
name: 'backstage-multi-output-format',
generateBundle(opts, bundle) {
const filter: (name: string) => boolean =
opts.format === 'cjs'
? s => s.endsWith(MOD_EXT)
: s => !s.endsWith(MOD_EXT);
// Delete any files that don't match the current output format
for (const name in bundle) {
if (filter(name)) {
delete bundle[name];
delete bundle[`${name}.map`];
}
}
},
renderDynamicImport(opts) {
if (opts.format === 'cjs') {
return {
left: 'import(',
right: ')',
};
}
return undefined;
},
};
}
export async function makeRollupConfigs(
options: BuildOptions,
): Promise<RollupOptions[]> {
@@ -120,18 +163,46 @@ export async function makeRollupConfigs(
const rewriteNodeModules = (name: string) =>
name.replaceAll('node_modules', 'node_modules_dist');
// For CommonJS we build both CommonJS and ESM output. Each of these outputs
// can output both .cjs and .mjs files. The files from each of these outputs
// will overlap, but we trim away files where the format doesn't match the
// file extensions. That way we are left with a combination of .cjs and .mjs
// files where the module format in the file matches the file extension.
if (options.outputs.has(Output.cjs)) {
output.push({
const defaultExt = targetPkg.type === 'module' ? MOD_EXT : CJS_EXT;
const outputOpts: OutputOptions = {
dir: distDir,
entryFileNames: chunkInfo =>
`${rewriteNodeModules(chunkInfo.name)}.cjs.js`,
chunkFileNames: `cjs/[name]-[hash].cjs.js`,
format: 'commonjs',
interop: 'compat',
entryFileNames(chunkInfo) {
const cleanName = rewriteNodeModules(chunkInfo.name);
const inputId = chunkInfo.facadeModuleId;
if (!inputId) {
return cleanName + defaultExt;
}
const inputExt = extname(inputId);
if (MODULE_EXTS.includes(inputExt)) {
return cleanName + MOD_EXT;
}
if (COMMONJS_EXTS.includes(inputExt)) {
return cleanName + CJS_EXT;
}
return cleanName + defaultExt;
},
sourcemap: true,
preserveModules: true,
preserveModulesRoot: `${targetDir}/src`,
exports: 'named',
plugins: [multiOutputFormat()],
};
output.push({
...outputOpts,
format: 'cjs',
});
output.push({
...outputOpts,
format: 'module',
});
}
if (options.outputs.has(Output.esm)) {
@@ -160,7 +231,19 @@ export async function makeRollupConfigs(
// All module imports are always marked as external
external,
plugins: [
resolve({ mainFields }),
resolve({
mainFields,
extensions: [
'.ts',
'.js',
'.tsx',
'.jsx',
'.mts',
'.cts',
'.mjs',
'.cjs',
],
}),
commonjs({
include: /node_modules/,
exclude: [/\/[^/]+\.(?:stories|test)\.[^/]+$/],
+1 -1
View File
@@ -107,7 +107,7 @@ export const buildPackage = async (options: BuildOptions) => {
const rollupConfigs = await makeRollupConfigs(options);
await fs.remove(paths.resolveTarget('dist'));
await fs.remove(resolvePath(options.targetDir ?? paths.targetDir, 'dist'));
const buildTasks = rollupConfigs.map(rollupBuild);
@@ -1 +1,2 @@
!node_modules
dist
@@ -1,3 +1,6 @@
{
"name": "pkg-default"
"name": "pkg-default",
"exports": {
".": "./main.ts"
}
}
@@ -21,10 +21,10 @@ import * as depModule from 'dep-module';
import * as depDefault from 'dep-default';
import { value as namedA } from './a-named';
import { value as namedB } from './b-named';
import cNamed from './c-named';
import * as cNamed from './c-named';
import defaultA from './a-default';
import defaultB from './b-default';
import cDefault from './c-default';
import * as cDefault from './c-default';
const { default: defaultC } = cDefault;
const { value: namedC } = cNamed;
@@ -16,6 +16,7 @@
import { execFileSync } from 'child_process';
import { resolve as resolvePath } from 'path';
import { Output, buildPackage } from '../../lib/builder';
const exportValues = {
all: {
@@ -95,7 +96,12 @@ describe('node runtime module transforms', () => {
dynCommonJs: expectedExports.commonJs,
dynDefault: expectedExports.commonJs,
dynModule: expectedExports.module,
dep: exportValues.all,
// TODO(Rugvip): Fix CommonJS import compat from modules
dep: {
...exportValues.all,
defaultC: { default: 'c' },
namedC: undefined,
},
dyn: exportValues.all,
});
});
@@ -156,3 +162,73 @@ describe('Jest runtime module transforms', () => {
});
});
});
describe('package build transforms', () => {
it('should build and load from commonjs format', async () => {
const pkgPath = resolvePath(__dirname, '__fixtures__/pkg-commonjs');
await buildPackage({
targetDir: pkgPath,
outputs: new Set([Output.cjs]),
workspacePackages: [],
});
const values = await import(resolvePath(pkgPath, 'dist/index.cjs')).then(
m => m.values,
);
expect(values).toEqual({
depCommonJs: expectedExports.commonJs,
depDefault: expectedExports.commonJs,
dynCommonJs: expectedExports.commonJs,
dynDefault: expectedExports.commonJs,
dynModule: expectedExports.module,
dep: exportValues.commonJs,
dyn: exportValues.all,
});
});
it('should build and load from default format', async () => {
const pkgPath = resolvePath(__dirname, '__fixtures__/pkg-default');
await buildPackage({
targetDir: pkgPath,
outputs: new Set([Output.cjs]),
workspacePackages: [],
});
const values = await import(resolvePath(pkgPath, 'dist/index.cjs')).then(
m => m.values,
);
expect(values).toEqual({
depCommonJs: expectedExports.commonJs,
depDefault: expectedExports.commonJs,
dynCommonJs: expectedExports.commonJs,
dynDefault: expectedExports.commonJs,
dynModule: expectedExports.module,
dep: exportValues.commonJs,
dyn: exportValues.all,
});
});
it('should build and load from module format', async () => {
const pkgPath = resolvePath(__dirname, '__fixtures__/pkg-module');
await buildPackage({
targetDir: pkgPath,
outputs: new Set([Output.cjs]),
workspacePackages: [],
});
const values = await import(resolvePath(pkgPath, 'dist/index.mjs')).then(
m => m.values,
);
expect(values).toEqual({
depCommonJs: expectedExports.commonJs,
depDefault: expectedExports.commonJs,
depModule: expectedExports.module,
dynCommonJs: expectedExports.commonJs,
dynDefault: expectedExports.commonJs,
dynModule: expectedExports.module,
// TODO(Rugvip): Fix CommonJS import compat from modules
dep: { ...exportValues.all, defaultC: { default: 'c' } },
dyn: exportValues.all,
});
});
});