diff --git a/packages/rollup/package.json b/packages/rollup/package.json index aad4428579e27..bd1cac756befb 100644 --- a/packages/rollup/package.json +++ b/packages/rollup/package.json @@ -43,7 +43,6 @@ "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.1", "rollup-plugin-typescript2": "0.36.0", - "rxjs": "^7.8.0", "tslib": "^2.3.0", "@nx/devkit": "file:../devkit", "@nx/js": "file:../js" diff --git a/packages/rollup/src/executors/rollup/lib/run-rollup.ts b/packages/rollup/src/executors/rollup/lib/run-rollup.ts deleted file mode 100644 index 774cd0780bc08..0000000000000 --- a/packages/rollup/src/executors/rollup/lib/run-rollup.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as rollup from 'rollup'; -import { from } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; - -export function runRollup(options: rollup.RollupOptions) { - return from(rollup.rollup(options)).pipe( - switchMap((bundle) => { - const outputOptions = Array.isArray(options.output) - ? options.output - : [options.output]; - return from( - Promise.all( - (>outputOptions).map((o) => - bundle.write(o) - ) - ) - ); - }), - map(() => ({ success: true })) - ); -} diff --git a/packages/rollup/src/executors/rollup/rollup.impl.spec.ts b/packages/rollup/src/executors/rollup/rollup.impl.spec.ts deleted file mode 100644 index f135948855dc6..0000000000000 --- a/packages/rollup/src/executors/rollup/rollup.impl.spec.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { ExecutorContext } from '@nx/devkit'; -import * as fs from 'fs'; -import * as rollup from 'rollup'; -import { RollupExecutorOptions } from './schema'; -import { createRollupOptions } from './rollup.impl'; -import { normalizeRollupExecutorOptions } from './lib/normalize'; - -jest.mock('rollup-plugin-copy', () => jest.fn()); -jest.mock('fs', () => ({ - ...jest.requireActual('fs'), - readdirSync: () => [], -})); -describe('rollupExecutor', () => { - let context: ExecutorContext; - let testOptions: RollupExecutorOptions; - - beforeEach(async () => { - context = { - root: '/root', - cwd: '/root', - projectsConfigurations: { - version: 2, - projects: {}, - }, - nxJsonConfiguration: {}, - isVerbose: false, - projectName: 'example', - targetName: 'build', - }; - testOptions = { - compiler: 'babel', - main: 'libs/ui/src/index.ts', - outputPath: 'dist/ui', - project: 'libs/ui/package.json', - tsConfig: 'libs/ui/tsconfig.json', - watch: false, - format: ['esm', 'cjs'], - }; - }); - - describe('createRollupOptions', () => { - it('should create rollup options for valid config', async () => { - const result: any = await createRollupOptions( - normalizeRollupExecutorOptions( - testOptions, - { root: '/root' } as any, - '/root/src' - ), - [], - context, - { name: 'example', version: '1.0' }, - '/root/src', - [] - ); - - expect(result.map((x) => x.output)).toEqual([ - { - dir: '/root/dist/ui', - format: 'esm', - name: 'Example', - chunkFileNames: '[name].esm.js', - entryFileNames: '[name].esm.js', - }, - { - dir: '/root/dist/ui', - format: 'cjs', - name: 'Example', - chunkFileNames: '[name].cjs.js', - entryFileNames: '[name].cjs.js', - }, - ]); - }); - - it('should handle custom config path', async () => { - jest.mock( - '/root/custom-rollup.config.ts', - () => (o) => ({ ...o, prop: 'my-val' }), - { virtual: true } - ); - const result: any = await createRollupOptions( - normalizeRollupExecutorOptions( - { ...testOptions, rollupConfig: 'custom-rollup.config.ts' }, - { root: '/root' } as any, - '/root/src' - ), - [], - context, - { name: 'example', version: '1.0' }, - '/root/src', - [] - ); - - expect(result.map((x) => x.prop)).toEqual(['my-val', 'my-val']); - }); - - it('should handle multiple custom config paths in order', async () => { - jest.mock( - '/root/custom-rollup-1.config.ts', - () => (o) => ({ ...o, prop1: 'my-val' }), - { virtual: true } - ); - jest.mock( - '/root/custom-rollup-2.config.ts', - () => (o) => ({ - ...o, - prop1: o.prop1 + '-my-val-2', - prop2: 'my-val-2', - }), - { virtual: true } - ); - const result: any = await createRollupOptions( - normalizeRollupExecutorOptions( - { - ...testOptions, - rollupConfig: [ - 'custom-rollup-1.config.ts', - 'custom-rollup-2.config.ts', - ], - }, - { root: '/root' } as any, - '/root/src' - ), - [], - context, - { name: 'example', version: '1.0' }, - '/root/src', - [] - ); - - expect(result.map((x) => x.prop1)).toEqual([ - 'my-val-my-val-2', - 'my-val-my-val-2', - ]); - expect(result.map((x) => x.prop2)).toEqual(['my-val-2', 'my-val-2']); - }); - - it(`should always use forward slashes for asset paths`, async () => { - await createRollupOptions( - { - ...normalizeRollupExecutorOptions( - testOptions, - { root: '/root' } as any, - '/root/src' - ), - assets: [ - { - glob: 'README.md', - input: 'C:\\windows\\path', - output: '.', - }, - ], - }, - [], - context, - { name: 'example', version: '1.0' }, - '/root/src', - [] - ); - - expect(require('rollup-plugin-copy')).toHaveBeenCalledWith({ - targets: [{ dest: '/root/dist/ui', src: 'C:/windows/path/README.md' }], - }); - }); - - it(`should treat npm dependencies as external if external is all`, async () => { - const options = await createRollupOptions( - normalizeRollupExecutorOptions( - { ...testOptions, external: 'all' }, - { root: '/root' } as any, - '/root/src' - ), - [], - context, - { name: 'example', version: '1.0' }, - '/root/src', - ['lodash'] - ); - - const external = options[0].external as rollup.IsExternal; - - expect(external('lodash', '', false)).toBe(true); - expect(external('lodash/fp', '', false)).toBe(true); - expect(external('rxjs', '', false)).toBe(false); - }); - - it(`should not treat npm dependencies as external if external is none`, async () => { - const options = await createRollupOptions( - normalizeRollupExecutorOptions( - { ...testOptions, external: 'none' }, - { root: '/root' } as any, - '/root/src' - ), - [], - context, - { name: 'example', version: '1.0' }, - '/root/src', - ['lodash'] - ); - - const external = options[0].external as rollup.IsExternal; - - expect(external('lodash', '', false)).toBe(false); - expect(external('lodash/fp', '', false)).toBe(false); - expect(external('rxjs', '', false)).toBe(false); - }); - - it(`should set external based on options`, async () => { - const options = await createRollupOptions( - normalizeRollupExecutorOptions( - { ...testOptions, external: ['rxjs'] }, - { root: '/root' } as any, - '/root/src' - ), - [], - context, - { name: 'example', version: '1.0' }, - '/root/src', - ['lodash'] - ); - - const external = options[0].external as rollup.IsExternal; - - expect(external('lodash', '', false)).toBe(false); - expect(external('lodash/fp', '', false)).toBe(false); - expect(external('rxjs', '', false)).toBe(true); - }); - }); -}); diff --git a/packages/rollup/src/executors/rollup/rollup.impl.ts b/packages/rollup/src/executors/rollup/rollup.impl.ts index 651933c140147..16788668595ff 100644 --- a/packages/rollup/src/executors/rollup/rollup.impl.ts +++ b/packages/rollup/src/executors/rollup/rollup.impl.ts @@ -3,12 +3,14 @@ import * as rollup from 'rollup'; import * as peerDepsExternal from 'rollup-plugin-peer-deps-external'; import { getBabelInputPlugin } from '@rollup/plugin-babel'; import { dirname, join, parse, resolve } from 'path'; -import { from, Observable, of } from 'rxjs'; -import { catchError, concatMap, last, scan, tap } from 'rxjs/operators'; -import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await'; import * as autoprefixer from 'autoprefixer'; -import type { ExecutorContext } from '@nx/devkit'; -import { joinPathFragments, logger, names, readJsonFile } from '@nx/devkit'; +import { + type ExecutorContext, + joinPathFragments, + logger, + names, + readJsonFile, +} from '@nx/devkit'; import { calculateProjectBuildableDependencies, computeCompilerOptionsPaths, @@ -19,7 +21,6 @@ import type { PackageJson } from 'nx/src/utils/package-json'; import { typeDefinitions } from '@nx/js/src/plugins/rollup/type-definitions'; import { AssetGlobPattern, RollupExecutorOptions } from './schema'; -import { runRollup } from './lib/run-rollup'; import { NormalizedRollupExecutorOptions, normalizeRollupExecutorOptions, @@ -29,11 +30,7 @@ import { deleteOutputDir } from '../../utils/fs'; import { swc } from './lib/swc-plugin'; import { updatePackageJson } from './lib/update-package-json'; import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; - -export type RollupExecutorEvent = { - success: boolean; - outfile?: string; -}; +import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable'; // These use require because the ES import isn't correct. const commonjs = require('@rollup/plugin-commonjs'); @@ -53,7 +50,7 @@ export async function* rollupExecutor( const project = context.projectsConfigurations.projects[context.projectName]; const sourceRoot = project.sourceRoot; - const { target, dependencies } = calculateProjectBuildableDependencies( + const { dependencies } = calculateProjectBuildableDependencies( context.taskGraph, context.projectGraph, context.root, @@ -87,70 +84,60 @@ export async function* rollupExecutor( const outfile = resolveOutfile(context, options); if (options.watch) { - const watcher = rollup.watch(rollupOptions); - return yield* eachValueFrom( - new Observable((obs) => { - watcher.on('event', (data) => { - if (data.code === 'START') { - logger.info(`Bundling ${context.projectName}...`); - } else if (data.code === 'END') { - updatePackageJson(options, packageJson); - logger.info('Bundle complete. Watching for file changes...'); - obs.next({ success: true, outfile }); - } else if (data.code === 'ERROR') { - logger.error(`Error during bundle: ${data.error.message}`); - obs.next({ success: false }); - } - }); - // Teardown logic. Close watcher when unsubscribed. - return () => watcher.close(); - }) - ); + // region Watch build + return createAsyncIterable(({ next }) => { + const watcher = rollup.watch(rollupOptions); + watcher.on('event', (data) => { + if (data.code === 'START') { + logger.info(`Bundling ${context.projectName}...`); + } else if (data.code === 'END') { + updatePackageJson(options, packageJson); + logger.info('Bundle complete. Watching for file changes...'); + next({ success: true, outfile }); + } else if (data.code === 'ERROR') { + logger.error(`Error during bundle: ${data.error.message}`); + next({ success: false }); + } + }); + const processExitListener = (signal?: number | NodeJS.Signals) => () => { + watcher.close(); + }; + process.once('SIGTERM', processExitListener); + process.once('SIGINT', processExitListener); + process.once('SIGQUIT', processExitListener); + }); + // endregion } else { - logger.info(`Bundling ${context.projectName}...`); + // region Single build + try { + logger.info(`Bundling ${context.projectName}...`); - // Delete output path before bundling - if (options.deleteOutputPath) { - deleteOutputDir(context.root, options.outputPath); - } + // Delete output path before bundling + if (options.deleteOutputPath) { + deleteOutputDir(context.root, options.outputPath); + } + + const start = process.hrtime.bigint(); + const bundle = await rollup.rollup(rollupOptions); + const output = Array.isArray(rollupOptions.output) + ? rollupOptions.output + : [rollupOptions.output]; + + for (const o of output) { + await bundle.write(o); + } - const start = process.hrtime.bigint(); - - return from(rollupOptions) - .pipe( - concatMap((opts) => - runRollup(opts).pipe( - catchError((e) => { - logger.error(`Error during bundle: ${e}`); - return of({ success: false }); - }) - ) - ), - scan( - (acc, result) => { - if (!acc.success) return acc; - return result; - }, - { success: true, outfile } - ), - last(), - tap({ - next: (result) => { - if (result.success) { - const end = process.hrtime.bigint(); - const duration = `${(Number(end - start) / 1_000_000_000).toFixed( - 2 - )}s`; - - updatePackageJson(options, packageJson); - logger.info(`⚡ Done in ${duration}`); - } else { - logger.error(`Bundle failed: ${context.projectName}`); - } - }, - }) - ) - .toPromise(); + const end = process.hrtime.bigint(); + const duration = `${(Number(end - start) / 1_000_000_000).toFixed(2)}s`; + + updatePackageJson(options, packageJson); + logger.info(`⚡ Done in ${duration}`); + return { success: true, outfile }; + } catch { + logger.error(`Bundle failed: ${context.projectName}`); + return { success: false }; + } + // endregion } } @@ -163,7 +150,7 @@ export async function createRollupOptions( packageJson: PackageJson, sourceRoot: string, npmDeps: string[] -): Promise { +): Promise { const useBabel = options.compiler === 'babel'; const useTsc = options.compiler === 'tsc'; const useSwc = options.compiler === 'swc'; @@ -196,150 +183,127 @@ export async function createRollupOptions( options.format = ['cjs']; } - const _rollupOptions = options.format.map(async (format, idx) => { - // Either we're generating only one format, so we should bundle types - // OR we are generating dual formats, so only bundle types for CJS. - const shouldBundleTypes = options.format.length === 1 || format === 'cjs'; - - const plugins = [ - copy({ - targets: convertCopyAssetsToRollupOptions( - options.outputPath, - options.assets - ), - }), - image(), - json(), - (useTsc || shouldBundleTypes) && - require('rollup-plugin-typescript2')({ - check: !options.skipTypeCheck, - tsconfig: options.tsConfig, - tsconfigOverride: { - compilerOptions: createTsCompilerOptions( - config, - dependencies, - options - ), - }, - }), - shouldBundleTypes && - typeDefinitions({ - main: options.main, - projectRoot: options.projectRoot, - }), - peerDepsExternal({ - packageJsonPath: options.project, - }), - postcss({ - inject: true, - extract: options.extractCss, - autoModules: true, - plugins: [autoprefixer], - use: { - less: { - javascriptEnabled: options.javascriptEnabled, - }, + const plugins = [ + copy({ + targets: convertCopyAssetsToRollupOptions( + options.outputPath, + options.assets + ), + }), + image(), + json(), + // Needed to generate type definitions, even if we're using babel or swc. + require('rollup-plugin-typescript2')({ + check: !options.skipTypeCheck, + tsconfig: options.tsConfig, + tsconfigOverride: { + compilerOptions: createTsCompilerOptions(config, dependencies, options), + }, + }), + + typeDefinitions({ + main: options.main, + projectRoot: options.projectRoot, + }), + peerDepsExternal({ + packageJsonPath: options.project, + }), + postcss({ + inject: true, + extract: options.extractCss, + autoModules: true, + plugins: [autoprefixer], + use: { + less: { + javascriptEnabled: options.javascriptEnabled, }, - }), - nodeResolve({ - preferBuiltins: true, + }, + }), + nodeResolve({ + preferBuiltins: true, + extensions: fileExtensions, + }), + useSwc && swc(), + useBabel && + getBabelInputPlugin({ + // Lets `@nx/js/babel` preset know that we are packaging. + caller: { + // @ts-ignore + // Ignoring type checks for caller since we have custom attributes + isNxPackage: true, + // Always target esnext and let rollup handle cjs + supportsStaticESM: true, + isModern: true, + }, + cwd: join(context.root, sourceRoot), + rootMode: options.babelUpwardRootMode ? 'upward' : undefined, + babelrc: true, extensions: fileExtensions, + babelHelpers: 'bundled', + skipPreflightCheck: true, // pre-flight check may yield false positives and also slows down the build + exclude: /node_modules/, }), - useSwc && swc(), - useBabel && - getBabelInputPlugin({ - // Lets `@nx/js/babel` preset know that we are packaging. - caller: { - // @ts-ignore - // Ignoring type checks for caller since we have custom attributes - isNxPackage: true, - // Always target esnext and let rollup handle cjs - supportsStaticESM: true, - isModern: true, - }, - cwd: join(context.root, sourceRoot), - rootMode: options.babelUpwardRootMode ? 'upward' : undefined, - babelrc: true, - extensions: fileExtensions, - babelHelpers: 'bundled', - skipPreflightCheck: true, // pre-flight check may yield false positives and also slows down the build - exclude: /node_modules/, - plugins: [ - format === 'esm' - ? undefined - : require.resolve('babel-plugin-transform-async-to-promises'), - ].filter(Boolean), - }), - commonjs(), - analyze(), - ]; - - let externalPackages = [ - ...Object.keys(packageJson.dependencies || {}), - ...Object.keys(packageJson.peerDependencies || {}), - ]; // If external is set to none, include all dependencies and peerDependencies in externalPackages - if (options.external === 'all') { - externalPackages = externalPackages - .concat(dependencies.map((d) => d.name)) - .concat(npmDeps); - } else if (Array.isArray(options.external) && options.external.length > 0) { - externalPackages = externalPackages.concat(options.external); - } - externalPackages = [...new Set(externalPackages)]; - - const mainEntryFileName = options.outputFileName || options.main; - const input: Record = {}; - input[parse(mainEntryFileName).name] = options.main; - options.additionalEntryPoints.forEach((entry) => { - input[parse(entry).name] = entry; - }); - - const rollupConfig = { - input, - output: { - format, - dir: `${options.outputPath}`, - name: names(context.projectName).className, - entryFileNames: `[name].${format}.js`, - chunkFileNames: `[name].${format}.js`, - }, - external: (id: string) => { - return externalPackages.some( - (name) => id === name || id.startsWith(`${name}/`) - ); // Could be a deep import - }, - plugins, - }; - - const userDefinedRollupConfigs = options.rollupConfig.map((plugin) => - loadConfigFile(plugin) - ); - let finalConfig: rollup.InputOptions = rollupConfig; - for (const _config of userDefinedRollupConfigs) { - const config = await _config; - if (typeof config === 'function') { - finalConfig = config(finalConfig, options); - } else { - finalConfig = { - ...finalConfig, - ...config, - plugins: [ - ...(finalConfig.plugins?.length > 0 ? finalConfig.plugins : []), - ...(config.plugins?.length > 0 ? config.plugins : []), - ], - }; - } - } + commonjs(), + analyze(), + ]; + + let externalPackages = [ + ...Object.keys(packageJson.dependencies || {}), + ...Object.keys(packageJson.peerDependencies || {}), + ]; // If external is set to none, include all dependencies and peerDependencies in externalPackages + if (options.external === 'all') { + externalPackages = externalPackages + .concat(dependencies.map((d) => d.name)) + .concat(npmDeps); + } else if (Array.isArray(options.external) && options.external.length > 0) { + externalPackages = externalPackages.concat(options.external); + } + externalPackages = [...new Set(externalPackages)]; - return finalConfig; + const mainEntryFileName = options.outputFileName || options.main; + const input: Record = {}; + input[parse(mainEntryFileName).name] = options.main; + options.additionalEntryPoints.forEach((entry) => { + input[parse(entry).name] = entry; }); - const rollupOptions = []; - for (const rollupOption of _rollupOptions) { - rollupOptions.push(await rollupOption); - } + const rollupConfig = { + input, + output: options.format.map((format) => ({ + format, + dir: `${options.outputPath}`, + name: names(context.projectName).className, + entryFileNames: `[name].${format}.js`, + chunkFileNames: `[name].${format}.js`, + })), + external: (id: string) => { + return externalPackages.some( + (name) => id === name || id.startsWith(`${name}/`) + ); // Could be a deep import + }, + plugins, + }; - return rollupOptions; + const userDefinedRollupConfigs = options.rollupConfig.map((plugin) => + loadConfigFile(plugin) + ); + let finalConfig: rollup.InputOptions = rollupConfig; + for (const _config of userDefinedRollupConfigs) { + const config = await _config; + if (typeof config === 'function') { + finalConfig = config(finalConfig, options); + } else { + finalConfig = { + ...finalConfig, + ...config, + plugins: [ + ...(finalConfig.plugins?.length > 0 ? finalConfig.plugins : []), + ...(config.plugins?.length > 0 ? config.plugins : []), + ], + }; + } + } + return finalConfig; } function createTsCompilerOptions(