cli: add module support for rollup build
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)\.[^/]+$/],
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user