diff --git a/docs/generated/packages/rollup/generators/init.json b/docs/generated/packages/rollup/generators/init.json index 7b41beaf84dcf..81fe6639068e3 100644 --- a/docs/generated/packages/rollup/generators/init.json +++ b/docs/generated/packages/rollup/generators/init.json @@ -24,6 +24,12 @@ "x-priority": "internal", "description": "Keep existing dependencies versions", "default": false + }, + "updatePackageScripts": { + "type": "boolean", + "x-priority": "internal", + "description": "Update `package.json` scripts with inferred targets", + "default": false } }, "required": [], diff --git a/e2e/rollup/src/rollup.test.ts b/e2e/rollup/src/rollup.test.ts index 716933f5ece3f..edc841a4fcbd0 100644 --- a/e2e/rollup/src/rollup.test.ts +++ b/e2e/rollup/src/rollup.test.ts @@ -161,4 +161,53 @@ describe('Rollup Plugin', () => { expect(() => runCLI(`build ${jsLib}`)).not.toThrow(); checkFilesExist(`dist/test/index.mjs.js`); }); + + it('should build correctly with crystal', () => { + // ARRANGE + updateFile( + `libs/test/src/index.ts`, + `export function helloWorld() { + console.log("hello world"); + }` + ); + updateFile(`libs/test/package.json`, JSON.stringify({ name: 'test' })); + updateFile( + `libs/test/rollup.config.js`, + `import babel from '@rollup/plugin-babel'; +import commonjs from '@rollup/plugin-commonjs'; +import typescript2 from 'rollup-plugin-typescript2'; + +const config = { + input: 'src/index.ts', + output: [ + { + file: 'dist/bundle.js', + format: 'cjs', + sourcemap: true + }, + { + file: 'dist/bundle.es.js', + format: 'es', + sourcemap: true + } + ], + plugins: [ + typescript2(), + babel({ babelHelpers: 'bundled' }), + commonjs(), + ] +}; + +export default config; +` + ); + // ACT + runCLI(`generate @nx/rollup:init --no-interactive`); + const output = runCLI(`build test`); + + // ASSERT + expect(output).toContain('Successfully ran target build for project test'); + checkFilesExist(`libs/test/dist/bundle.js`); + checkFilesExist(`libs/test/dist/bundle.es.js`); + }); }); diff --git a/packages/nx/src/command-line/init/init-v2.ts b/packages/nx/src/command-line/init/init-v2.ts index 435740560f73b..95caef268d6d0 100644 --- a/packages/nx/src/command-line/init/init-v2.ts +++ b/packages/nx/src/command-line/init/init-v2.ts @@ -145,6 +145,7 @@ const npmPackageToPluginMap: Record = { vite: '@nx/vite', vitest: '@nx/vite', webpack: '@nx/webpack', + rollup: '@nx/rollup', // Testing tools jest: '@nx/jest', cypress: '@nx/cypress', diff --git a/packages/rollup/plugin.ts b/packages/rollup/plugin.ts new file mode 100644 index 0000000000000..5fe67b97f727b --- /dev/null +++ b/packages/rollup/plugin.ts @@ -0,0 +1 @@ +export { createNodes, RollupPluginOptions } from './src/plugins/plugin'; diff --git a/packages/rollup/project.json b/packages/rollup/project.json index d82412303d1e8..8e79971e9ca3a 100644 --- a/packages/rollup/project.json +++ b/packages/rollup/project.json @@ -8,6 +8,11 @@ "build-base": { "executor": "@nx/js:tsc", "options": { + "generateExportsField": true, + "additionalEntryPoints": [ + "{projectRoot}/{executors,generators,migrations}.json", + "{projectRoot}/plugin.ts" + ], "assets": [ { "input": "packages/rollup", diff --git a/packages/rollup/src/generators/init/init.ts b/packages/rollup/src/generators/init/init.ts index 492d6b8a6eaa9..dda52be7c5352 100644 --- a/packages/rollup/src/generators/init/init.ts +++ b/packages/rollup/src/generators/init/init.ts @@ -3,9 +3,37 @@ import { formatFiles, GeneratorCallback, Tree, + readNxJson, + updateNxJson, } from '@nx/devkit'; import { nxVersion } from '../../utils/versions'; import { Schema } from './schema'; +import { updatePackageScripts } from '@nx/devkit/src/utils/update-package-scripts'; +import { createNodes } from '../../plugins/plugin'; + +function addPlugin(tree: Tree) { + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + + for (const plugin of nxJson.plugins) { + if ( + typeof plugin === 'string' + ? plugin === '@nx/rollup/plugin' + : plugin.plugin === '@nx/rollup/plugin' + ) { + return; + } + } + + nxJson.plugins.push({ + plugin: '@nx/rollup/plugin', + options: { + buildTargetName: 'build', + }, + }); + + updateNxJson(tree, nxJson); +} export async function rollupInitGenerator(tree: Tree, schema: Schema) { let task: GeneratorCallback = () => {}; @@ -20,6 +48,15 @@ export async function rollupInitGenerator(tree: Tree, schema: Schema) { ); } + schema.addPlugin ??= process.env.NX_ADD_PLUGINS !== 'false'; + if (schema.addPlugin) { + addPlugin(tree); + } + + if (schema.updatePackageScripts) { + await updatePackageScripts(tree, createNodes); + } + if (!schema.skipFormat) { await formatFiles(tree); } diff --git a/packages/rollup/src/generators/init/schema.d.ts b/packages/rollup/src/generators/init/schema.d.ts index 9708e88a1511f..7c2c983ef70ed 100644 --- a/packages/rollup/src/generators/init/schema.d.ts +++ b/packages/rollup/src/generators/init/schema.d.ts @@ -2,4 +2,6 @@ export interface Schema { skipFormat?: boolean; skipPackageJson?: boolean; keepExistingVersions?: boolean; + updatePackageScripts?: boolean; + addPlugin?: boolean; } diff --git a/packages/rollup/src/generators/init/schema.json b/packages/rollup/src/generators/init/schema.json index 09f322bd1c35b..6887084f08278 100644 --- a/packages/rollup/src/generators/init/schema.json +++ b/packages/rollup/src/generators/init/schema.json @@ -21,6 +21,12 @@ "x-priority": "internal", "description": "Keep existing dependencies versions", "default": false + }, + "updatePackageScripts": { + "type": "boolean", + "x-priority": "internal", + "description": "Update `package.json` scripts with inferred targets", + "default": false } }, "required": [] diff --git a/packages/rollup/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/rollup/src/plugins/__snapshots__/plugin.spec.ts.snap new file mode 100644 index 0000000000000..fc1af6b0ad57b --- /dev/null +++ b/packages/rollup/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@nx/rollup/plugin non-root project should create nodes 1`] = ` +{ + "projects": { + "mylib": { + "root": "mylib", + "targets": { + "build": { + "cache": true, + "command": "rollup -c rollup.config.js", + "dependsOn": [ + "^build", + ], + "inputs": [ + "production", + "^production", + ], + "options": { + "cwd": "mylib", + }, + "outputs": [ + "{workspaceRoot}/mylib/build", + "{workspaceRoot}/mylib/dist", + ], + }, + }, + }, + }, +} +`; + +exports[`@nx/rollup/plugin root project should create nodes 1`] = ` +{ + "projects": { + ".": { + "root": ".", + "targets": { + "build": { + "cache": true, + "command": "rollup -c rollup.config.js", + "dependsOn": [ + "^build", + ], + "inputs": [ + "production", + "^production", + ], + "options": { + "cwd": ".", + }, + "outputs": [ + "{workspaceRoot}/dist", + ], + }, + }, + }, + }, +} +`; diff --git a/packages/rollup/src/plugins/plugin.spec.ts b/packages/rollup/src/plugins/plugin.spec.ts new file mode 100644 index 0000000000000..82b4bca4ae4ad --- /dev/null +++ b/packages/rollup/src/plugins/plugin.spec.ts @@ -0,0 +1,155 @@ +import { type CreateNodesContext, joinPathFragments } from '@nx/devkit'; +import { createNodes } from './plugin'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; + +describe('@nx/rollup/plugin', () => { + let createNodesFunction = createNodes[1]; + let context: CreateNodesContext; + let cwd = process.cwd(); + + describe('root project', () => { + const tempFs = new TempFs('test'); + + beforeEach(() => { + context = { + nxJsonConfiguration: { + targetDefaults: { + build: { + cache: false, + inputs: ['foo', '^foo'], + }, + }, + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + }; + + tempFs.createFileSync('package.json', JSON.stringify({ name: 'mylib' })); + tempFs.createFileSync( + 'src/index.js', + `export function main() { + console.log("hello world"); + }` + ); + tempFs.createFileSync( + 'rollup.config.js', + ` +const config = { + input: 'src/index.js', + output: [ + { + file: 'dist/bundle.js', + format: 'cjs', + sourcemap: true + }, + { + file: 'dist/bundle.es.js', + format: 'es', + sourcemap: true + } + ], + plugins: [], +}; + +module.exports = config; + ` + ); + + process.chdir(tempFs.tempDir); + }); + + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + process.chdir(cwd); + }); + + it('should create nodes', async () => { + // ACT + const nodes = await createNodesFunction( + 'rollup.config.js', + { + buildTargetName: 'build', + }, + context + ); + + // ASSERT + expect(nodes).toMatchSnapshot(); + }); + }); + describe('non-root project', () => { + const tempFs = new TempFs('test'); + + beforeEach(() => { + context = { + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + }; + + tempFs.createFileSync( + 'mylib/package.json', + JSON.stringify({ name: 'mylib' }) + ); + tempFs.createFileSync( + 'mylib/src/index.js', + `export function main() { + console.log("hello world"); + }` + ); + tempFs.createFileSync( + 'mylib/rollup.config.js', + ` +const config = { + input: 'src/index.js', + output: [ + { + file: 'build/bundle.js', + format: 'cjs', + sourcemap: true + }, + { + file: 'dist/bundle.es.js', + format: 'es', + sourcemap: true + } + ], + plugins: [], +}; + +module.exports = config; + ` + ); + + process.chdir(tempFs.tempDir); + }); + + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + process.chdir(cwd); + }); + + it('should create nodes', async () => { + // ACT + const nodes = await createNodesFunction( + 'mylib/rollup.config.js', + { + buildTargetName: 'build', + }, + context + ); + + // ASSERT + expect(nodes).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/rollup/src/plugins/plugin.ts b/packages/rollup/src/plugins/plugin.ts new file mode 100644 index 0000000000000..af770e8d8e35d --- /dev/null +++ b/packages/rollup/src/plugins/plugin.ts @@ -0,0 +1,155 @@ +import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; +import { basename, dirname, join } from 'path'; +import { existsSync, readdirSync } from 'fs'; +import { + type TargetConfiguration, + type CreateDependencies, + type CreateNodes, + readJsonFile, + writeJsonFile, + detectPackageManager, + CreateNodesContext, + joinPathFragments, +} from '@nx/devkit'; +import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; +import { getLockFileName } from '@nx/js'; +import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; +import { type RollupOptions } from 'rollup'; +import * as loadConfigFile from 'rollup/dist/loadConfigFile'; + +const cachePath = join(projectGraphCacheDirectory, 'rollup.hash'); +const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; +const calculatedTargets: Record< + string, + Record +> = {}; + +function readTargetsCache(): Record< + string, + Record +> { + return readJsonFile(cachePath); +} + +function writeTargetsToCache( + targets: Record> +) { + writeJsonFile(cachePath, targets); +} + +export const createDependencies: CreateDependencies = () => { + writeTargetsToCache(calculatedTargets); + return []; +}; + +export interface RollupPluginOptions { + buildTargetName?: string; +} + +export const createNodes: CreateNodes = [ + '**/rollup.config.{js,cjs,mjs}', + async (configFilePath, options, context) => { + const projectRoot = dirname(configFilePath); + const fullyQualifiedProjectRoot = join(context.workspaceRoot, projectRoot); + // Do not create a project if package.json and project.json do not exist + const siblingFiles = readdirSync(fullyQualifiedProjectRoot); + if ( + !siblingFiles.includes('package.json') && + !siblingFiles.includes('project.json') + ) { + return {}; + } + + options = normalizeOptions(options); + + const hash = calculateHashForCreateNodes(projectRoot, options, context, [ + getLockFileName(detectPackageManager(context.workspaceRoot)), + ]); + + const targets = targetsCache[hash] + ? targetsCache[hash] + : await buildRollupTarget(configFilePath, projectRoot, options, context); + + calculatedTargets[hash] = targets; + return { + projects: { + [projectRoot]: { + root: projectRoot, + targets, + }, + }, + }; + }, +]; + +async function buildRollupTarget( + configFilePath: string, + projectRoot: string, + options: RollupPluginOptions, + context: CreateNodesContext +): Promise> { + const namedInputs = getNamedInputs(projectRoot, context); + const rollupConfig = ( + (await loadConfigFile( + joinPathFragments(context.workspaceRoot, configFilePath) + )) as { options: RollupOptions[] } + ).options; + const outputs = getOutputs(rollupConfig, projectRoot); + + const targets: Record = {}; + targets[options.buildTargetName] = { + command: `rollup -c ${basename(configFilePath)}`, + options: { cwd: projectRoot }, + cache: true, + dependsOn: [`^${options.buildTargetName}`], + inputs: [ + ...('production' in namedInputs + ? ['production', '^production'] + : ['default', '^default']), + ], + outputs, + }; + return targets; +} + +function getOutputs( + rollupConfigs: RollupOptions[], + projectRoot: string +): string[] { + const outputs = new Set(); + for (const rollupConfig of rollupConfigs) { + if (rollupConfig.output) { + const rollupConfigOutputs = []; + if (Array.isArray(rollupConfig.output)) { + rollupConfigOutputs.push(...rollupConfig.output); + } else { + rollupConfigOutputs.push(rollupConfig.output); + } + + for (const output of rollupConfigOutputs) { + const outputPathFromConfig = output.dir + ? output.dir + : output.file + ? dirname(output.file) + : 'dist'; + const outputPath = + projectRoot === '.' + ? joinPathFragments(`{workspaceRoot}`, outputPathFromConfig) + : joinPathFragments( + `{workspaceRoot}`, + projectRoot, + outputPathFromConfig + ); + outputs.add(outputPath); + } + } + } + return Array.from(outputs); +} + +function normalizeOptions(options: RollupPluginOptions) { + options ??= {}; + options.buildTargetName ??= 'build'; + + return options; +}