feat(bundling): extract rollup plugins into withNx function for use with run-commands (#26168)

This PR adds `withNx` function to `@nx/rollup/with-nx` so it can be used
in `rollup.config.js` to replicate what `@nx/rollup:rollup` executor
does without needing to use the executor.

e.g. 

```js
// rollup.config.js
const { withNx } = require("@nx/rollup/with-nx");

module.exports = withNx(
  {
    main: "./src/index.ts",
    outputPath: "./dist",
    tsConfig: "./tsconfig.lib.json",
    compiler: "babel",
    external: ["react", "react-dom", "react/jsx-runtime"],
    format: ["esm"],
    assets: [{ input: ".", output: ".", glob: "README.md" }],
  },
  {
    // Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
    // e.g.
    // output: { sourcemap: true },
  }
);
```


## Notes

1. Existing `@nx/rollup:rollup` continues to encapsulate rollup options
and will not support an isolated mode.
2. Newly created JS and React libs with `--bundler=rollup` will use the
new `withNx` function and explicit `rollup.config.js`.
3. If `NX_ADD_PLUGINS=false` or `useInferencePlugins: false` is set,
then new projects will continue to use the `@nx/rollup:rollup` executor.
This commit is contained in:
Jack Hsu 2024-05-31 10:50:10 -04:00 committed by GitHub
parent c05e4ac268
commit 4e49d527ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1665 additions and 747 deletions

View File

@ -0,0 +1,202 @@
import {
checkFilesDoNotExist,
checkFilesExist,
cleanupProject,
getSize,
killPorts,
newProject,
readFile,
readJson,
rmDist,
runCLI,
runCLIAsync,
tmpProjPath,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { names } from '@nx/devkit';
import { join } from 'path';
describe('Build React libraries and apps', () => {
/**
* Graph:
*
* childLib
* /
* app => parentLib =>
* \
* childLib2
*
*/
let app: string;
let parentLib: string;
let childLib: string;
let childLib2: string;
let proj: string;
beforeEach(async () => {
process.env.NX_ADD_PLUGINS = 'false';
app = uniq('app');
parentLib = uniq('parentlib');
childLib = uniq('childlib');
childLib2 = uniq('childlib2');
proj = newProject({ packages: ['@nx/react'] });
// create dependencies by importing
const createDep = (parent, children: string[]) => {
updateFile(
`libs/${parent}/src/index.ts`,
`
export * from './lib/${parent}';
${children
.map(
(entry) =>
`import { ${
names(entry).className
} } from '@${proj}/${entry}'; console.log(${
names(entry).className
});`
)
.join('\n')}
`
);
};
runCLI(`generate @nx/react:app ${app} `);
updateJson('nx.json', (json) => ({
...json,
generators: {
...json.generators,
'@nx/react': {
library: {
unitTestRunner: 'none',
},
},
},
}));
// generate buildable libs
runCLI(
`generate @nx/react:library ${parentLib} --bundler=rollup --importPath=@${proj}/${parentLib} --no-interactive --unitTestRunner=jest --skipFormat`
);
runCLI(
`generate @nx/react:library ${childLib} --bundler=rollup --importPath=@${proj}/${childLib} --no-interactive --unitTestRunner=jest --skipFormat`
);
runCLI(
`generate @nx/react:library ${childLib2} --bundler=rollup --importPath=@${proj}/${childLib2} --no-interactive --unitTestRunner=jest --skipFormat`
);
createDep(parentLib, [childLib, childLib2]);
updateFile(
`apps/${app}/src/main.tsx`,
`
import {${names(parentLib).className}} from "@${proj}/${parentLib}";
console.log(${names(parentLib).className});
`
);
// Add assets to child lib
updateJson(join('libs', childLib, 'project.json'), (json) => {
json.targets.build.options.assets = [`libs/${childLib}/src/assets`];
return json;
});
updateFile(`libs/${childLib}/src/assets/hello.txt`, 'Hello World!');
});
afterEach(() => {
killPorts();
cleanupProject();
delete process.env.NX_ADD_PLUGINS;
});
describe('Buildable libraries', () => {
it('should build libraries with and without dependencies', () => {
/*
* 1. Without dependencies
*/
runCLI(`build ${childLib}`);
runCLI(`build ${childLib2}`);
checkFilesExist(`dist/libs/${childLib}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib2}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib}/assets/hello.txt`);
checkFilesExist(`dist/libs/${childLib2}/README.md`);
/*
* 2. With dependencies without existing dist
*/
rmDist();
runCLI(`build ${parentLib} --skip-nx-cache`);
checkFilesExist(`dist/libs/${parentLib}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib}/index.esm.js`);
checkFilesExist(`dist/libs/${childLib2}/index.esm.js`);
expect(readFile(`dist/libs/${childLib}/index.esm.js`)).not.toContain(
'react/jsx-dev-runtime'
);
expect(readFile(`dist/libs/${childLib}/index.esm.js`)).toContain(
'react/jsx-runtime'
);
});
it('should support --format option', () => {
updateFile(
`libs/${childLib}/src/index.ts`,
(s) => `${s}
export async function f() { return 'a'; }
export async function g() { return 'b'; }
export async function h() { return 'c'; }
`
);
runCLI(`build ${childLib} --format cjs,esm`);
checkFilesExist(`dist/libs/${childLib}/index.cjs.js`);
checkFilesExist(`dist/libs/${childLib}/index.esm.js`);
const cjsPackageSize = getSize(
tmpProjPath(`dist/libs/${childLib}/index.cjs.js`)
);
const esmPackageSize = getSize(
tmpProjPath(`dist/libs/${childLib}/index.esm.js`)
);
// This is a loose requirement that ESM should be smaller than CJS output.
expect(esmPackageSize).toBeLessThanOrEqual(cjsPackageSize);
});
it('should build an app composed out of buildable libs', () => {
const buildFromSource = runCLI(
`build ${app} --buildLibsFromSource=false`
);
expect(buildFromSource).toContain('Successfully ran target build');
checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);
}, 1000000);
it('should not create a dist folder if there is an error', async () => {
const libName = uniq('lib');
runCLI(
`generate @nx/react:lib ${libName} --bundler=rollup --importPath=@${proj}/${libName} --no-interactive --unitTestRunner=jest`
);
const mainPath = `libs/${libName}/src/lib/${libName}.tsx`;
updateFile(mainPath, `${readFile(mainPath)}\n console.log(a);`); // should error - "a" will be undefined
await expect(runCLIAsync(`build ${libName}`)).rejects.toThrow(
/Bundle failed/
);
expect(() => {
checkFilesExist(`dist/libs/${libName}/package.json`);
}).toThrow();
}, 250000);
});
});

View File

@ -1,16 +1,12 @@
import {
checkFilesDoNotExist,
checkFilesExist,
cleanupProject,
getSize,
killPorts,
newProject,
readFile,
readJson,
rmDist,
runCLI,
runCLIAsync,
tmpProjPath,
uniq,
updateFile,
updateJson,
@ -37,7 +33,6 @@ describe('Build React libraries and apps', () => {
let proj: string;
beforeEach(async () => {
process.env.NX_ADD_PLUGINS = 'false';
app = uniq('app');
parentLib = uniq('parentlib');
childLib = uniq('childlib');
@ -100,17 +95,28 @@ describe('Build React libraries and apps', () => {
);
// Add assets to child lib
updateJson(join('libs', childLib, 'project.json'), (json) => {
json.targets.build.options.assets = [`libs/${childLib}/src/assets`];
return json;
});
updateFile(
join('libs', childLib, 'rollup.config.js'),
`const { withNx } = require('@nx/rollup/with-nx');
module.exports = withNx(
{
main: './src/index.ts',
outputPath: '../../dist/libs/${childLib}',
tsConfig: './tsconfig.lib.json',
compiler: 'babel',
external: ['react', 'react-dom', 'react/jsx-runtime'],
format: ['esm'],
assets: ['./src/assets'],
}
);
`
);
updateFile(`libs/${childLib}/src/assets/hello.txt`, 'Hello World!');
});
afterEach(() => {
killPorts();
cleanupProject();
delete process.env.NX_ADD_PLUGINS;
});
describe('Buildable libraries', () => {
@ -147,32 +153,6 @@ describe('Build React libraries and apps', () => {
);
});
it('should support --format option', () => {
updateFile(
`libs/${childLib}/src/index.ts`,
(s) => `${s}
export async function f() { return 'a'; }
export async function g() { return 'b'; }
export async function h() { return 'c'; }
`
);
runCLI(`build ${childLib} --format cjs,esm`);
checkFilesExist(`dist/libs/${childLib}/index.cjs.js`);
checkFilesExist(`dist/libs/${childLib}/index.esm.js`);
const cjsPackageSize = getSize(
tmpProjPath(`dist/libs/${childLib}/index.cjs.js`)
);
const esmPackageSize = getSize(
tmpProjPath(`dist/libs/${childLib}/index.esm.js`)
);
// This is a loose requirement that ESM should be smaller than CJS output.
expect(esmPackageSize).toBeLessThanOrEqual(cjsPackageSize);
});
it('should preserve the tsconfig target set by user', () => {
// Setup
const myLib = uniq('my-lib');
@ -224,14 +204,6 @@ export async function h() { return 'c'; }
expect(content).toContain('function __generator(thisArg, body) {');
});
it('should build an app composed out of buildable libs', () => {
const buildFromSource = runCLI(
`build ${app} --buildLibsFromSource=false`
);
expect(buildFromSource).toContain('Successfully ran target build');
checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);
}, 1000000);
it('should not create a dist folder if there is an error', async () => {
const libName = uniq('lib');
@ -243,7 +215,7 @@ export async function h() { return 'c'; }
updateFile(mainPath, `${readFile(mainPath)}\n console.log(a);`); // should error - "a" will be undefined
await expect(runCLIAsync(`build ${libName}`)).rejects.toThrow(
/Bundle failed/
/Command failed/
);
expect(() => {
checkFilesExist(`dist/libs/${libName}/package.json`);

View File

@ -0,0 +1,201 @@
import {
checkFilesExist,
cleanupProject,
newProject,
packageInstall,
readJson,
rmDist,
runCLI,
runCommand,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { join } from 'path';
describe('Rollup Plugin', () => {
let originalAddPluginsEnv: string | undefined;
beforeAll(() => {
originalAddPluginsEnv = process.env.NX_ADD_PLUGINS;
process.env.NX_ADD_PLUGINS = 'false';
newProject({ packages: ['@nx/rollup', '@nx/js'] });
});
afterAll(() => {
process.env.NX_ADD_PLUGINS = originalAddPluginsEnv;
cleanupProject();
});
it('should be able to setup project to build node programs with rollup and different compilers', async () => {
const myPkg = uniq('my-pkg');
runCLI(`generate @nx/js:lib ${myPkg} --bundler=none`);
updateFile(`libs/${myPkg}/src/index.ts`, `console.log('Hello');\n`);
// babel (default)
runCLI(
`generate @nx/rollup:configuration ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts`
);
rmDist();
runCLI(`build ${myPkg} --format=cjs,esm --generateExportsField`);
checkFilesExist(`dist/libs/${myPkg}/index.cjs.d.ts`);
expect(readJson(`dist/libs/${myPkg}/package.json`).exports).toEqual({
'.': {
module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js',
},
'./package.json': './package.json',
});
let output = runCommand(`node dist/libs/${myPkg}/index.cjs.js`);
expect(output).toMatch(/Hello/);
updateJson(join('libs', myPkg, 'project.json'), (config) => {
delete config.targets.build;
return config;
});
// swc
runCLI(
`generate @nx/rollup:configuration ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts --compiler=swc`
);
rmDist();
runCLI(`build ${myPkg} --format=cjs,esm --generateExportsField`);
output = runCommand(`node dist/libs/${myPkg}/index.cjs.js`);
expect(output).toMatch(/Hello/);
updateJson(join('libs', myPkg, 'project.json'), (config) => {
delete config.targets.build;
return config;
});
// tsc
runCLI(
`generate @nx/rollup:configuration ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts --compiler=tsc`
);
rmDist();
runCLI(`build ${myPkg} --format=cjs,esm --generateExportsField`);
output = runCommand(`node dist/libs/${myPkg}/index.cjs.js`);
expect(output).toMatch(/Hello/);
}, 500000);
it('should support additional entry-points', async () => {
const myPkg = uniq('my-pkg');
runCLI(`generate @nx/js:lib ${myPkg} --bundler=none`);
runCLI(
`generate @nx/rollup:configuration ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts --compiler=tsc`
);
updateJson(join('libs', myPkg, 'project.json'), (config) => {
config.targets.build.options.format = ['cjs', 'esm'];
config.targets.build.options.generateExportsField = true;
config.targets.build.options.additionalEntryPoints = [
`libs/${myPkg}/src/{foo,bar}.ts`,
];
return config;
});
updateFile(`libs/${myPkg}/src/foo.ts`, `export const foo = 'foo';`);
updateFile(`libs/${myPkg}/src/bar.ts`, `export const bar = 'bar';`);
runCLI(`build ${myPkg}`);
checkFilesExist(`dist/libs/${myPkg}/index.esm.js`);
checkFilesExist(`dist/libs/${myPkg}/index.cjs.js`);
checkFilesExist(`dist/libs/${myPkg}/index.cjs.d.ts`);
checkFilesExist(`dist/libs/${myPkg}/foo.esm.js`);
checkFilesExist(`dist/libs/${myPkg}/foo.cjs.js`);
checkFilesExist(`dist/libs/${myPkg}/bar.esm.js`);
checkFilesExist(`dist/libs/${myPkg}/bar.cjs.js`);
expect(readJson(`dist/libs/${myPkg}/package.json`).exports).toEqual({
'./package.json': './package.json',
'.': {
module: './index.esm.js',
import: './index.cjs.mjs',
default: './index.cjs.js',
},
'./bar': {
module: './bar.esm.js',
import: './bar.cjs.mjs',
default: './bar.cjs.js',
},
'./foo': {
module: './foo.esm.js',
import: './foo.cjs.mjs',
default: './foo.cjs.js',
},
});
});
it('should be able to build libs generated with @nx/js:lib --bundler rollup', () => {
const jsLib = uniq('jslib');
runCLI(`generate @nx/js:lib ${jsLib} --bundler rollup`);
expect(() => runCLI(`build ${jsLib}`)).not.toThrow();
});
it('should be able to build libs generated with @nx/js:lib --bundler rollup with a custom rollup.config.{cjs|mjs}', () => {
const jsLib = uniq('jslib');
runCLI(`generate @nx/js:lib ${jsLib} --bundler rollup`);
updateFile(
`libs/${jsLib}/rollup.config.cjs`,
`module.exports = {
output: {
format: "cjs",
dir: "dist/test",
name: "Mylib",
entryFileNames: "[name].cjs.js",
chunkFileNames: "[name].cjs.js"
}
}`
);
updateJson(join('libs', jsLib, 'project.json'), (config) => {
config.targets.build.options.rollupConfig = `libs/${jsLib}/rollup.config.cjs`;
return config;
});
expect(() => runCLI(`build ${jsLib}`)).not.toThrow();
checkFilesExist(`dist/test/index.cjs.js`);
updateFile(
`libs/${jsLib}/rollup.config.mjs`,
`export default {
output: {
format: "es",
dir: "dist/test",
name: "Mylib",
entryFileNames: "[name].mjs.js",
chunkFileNames: "[name].mjs.js"
}
}`
);
updateJson(join('libs', jsLib, 'project.json'), (config) => {
config.targets.build.options.rollupConfig = `libs/${jsLib}/rollup.config.mjs`;
return config;
});
expect(() => runCLI(`build ${jsLib}`)).not.toThrow();
checkFilesExist(`dist/test/index.mjs.js`);
});
it('should support array config from rollup.config.js', () => {
const jsLib = uniq('jslib');
runCLI(`generate @nx/js:lib ${jsLib} --bundler rollup --verbose`);
updateFile(
`libs/${jsLib}/rollup.config.js`,
`module.exports = (config) => [{
...config,
output: {
format: "esm",
dir: "dist/test",
name: "Mylib",
entryFileNames: "[name].js",
chunkFileNames: "[name].js"
}
}]`
);
updateJson(join('libs', jsLib, 'project.json'), (config) => {
config.targets.build.options.rollupConfig = `libs/${jsLib}/rollup.config.js`;
return config;
});
expect(() => runCLI(`build ${jsLib} --format=esm`)).not.toThrow();
checkFilesExist(`dist/test/index.js`);
});
});

View File

@ -9,9 +9,7 @@ import {
runCommand,
uniq,
updateFile,
updateJson,
} from '@nx/e2e/utils';
import { join } from 'path';
describe('Rollup Plugin', () => {
beforeAll(() => newProject({ packages: ['@nx/rollup', '@nx/js'] }));
@ -19,15 +17,30 @@ describe('Rollup Plugin', () => {
it('should be able to setup project to build node programs with rollup and different compilers', async () => {
const myPkg = uniq('my-pkg');
runCLI(`generate @nx/js:lib ${myPkg} --bundler=none`);
runCLI(`generate @nx/js:lib ${myPkg} --bundler=rollup`);
updateFile(`libs/${myPkg}/src/index.ts`, `console.log('Hello');\n`);
// babel (default)
runCLI(
`generate @nx/rollup:configuration ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts`
`generate @nx/rollup:configuration ${myPkg} --tsConfig=./tsconfig.lib.json --main=./src/index.ts`
);
updateFile(
`libs/${myPkg}/rollup.config.js`,
`
const { withNx } = require('@nx/rollup/with-nx');
module.exports = withNx({
outputPath: '../../dist/libs/${myPkg}',
main: './src/index.ts',
tsConfig: './tsconfig.lib.json',
compiler: 'babel',
generateExportsField: true,
additionalEntryPoints: ['./src/{foo,bar}.ts'],
format: ['cjs', 'esm']
});
`
);
rmDist();
runCLI(`build ${myPkg} --format=cjs,esm --generateExportsField`);
runCLI(`build ${myPkg}`);
checkFilesExist(`dist/libs/${myPkg}/index.cjs.d.ts`);
expect(readJson(`dist/libs/${myPkg}/package.json`).exports).toEqual({
'.': {
@ -40,31 +53,51 @@ describe('Rollup Plugin', () => {
let output = runCommand(`node dist/libs/${myPkg}/index.cjs.js`);
expect(output).toMatch(/Hello/);
updateJson(join('libs', myPkg, 'project.json'), (config) => {
delete config.targets.build;
return config;
});
// swc
runCLI(
`generate @nx/rollup:configuration ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts --compiler=swc`
`generate @nx/rollup:configuration ${myPkg} --tsConfig=./tsconfig.lib.json --main=./src/index.ts --compiler=swc`
);
updateFile(
`libs/${myPkg}/rollup.config.js`,
`
const { withNx } = require('@nx/rollup/with-nx');
module.exports = withNx({
outputPath: '../../dist/libs/${myPkg}',
main: './src/index.ts',
tsConfig: './tsconfig.lib.json',
compiler: 'swc',
generateExportsField: true,
additionalEntryPoints: ['./src/{foo,bar}.ts'],
format: ['cjs', 'esm']
});
`
);
rmDist();
runCLI(`build ${myPkg} --format=cjs,esm --generateExportsField`);
runCLI(`build ${myPkg}`);
output = runCommand(`node dist/libs/${myPkg}/index.cjs.js`);
expect(output).toMatch(/Hello/);
updateJson(join('libs', myPkg, 'project.json'), (config) => {
delete config.targets.build;
return config;
});
// tsc
runCLI(
`generate @nx/rollup:configuration ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts --compiler=tsc`
`generate @nx/rollup:configuration ${myPkg} --tsConfig=./tsconfig.lib.json --main=./src/index.ts --compiler=tsc`
);
updateFile(
`libs/${myPkg}/rollup.config.js`,
`
const { withNx } = require('@nx/rollup/with-nx');
module.exports = withNx({
outputPath: '../../dist/libs/${myPkg}',
main: './src/index.ts',
tsConfig: './tsconfig.lib.json',
compiler: 'tsc',
generateExportsField: true,
additionalEntryPoints: ['./src/{foo,bar}.ts'],
format: ['cjs', 'esm']
});
`
);
rmDist();
runCLI(`build ${myPkg} --format=cjs,esm --generateExportsField`);
runCLI(`build ${myPkg}`);
output = runCommand(`node dist/libs/${myPkg}/index.cjs.js`);
expect(output).toMatch(/Hello/);
}, 500000);
@ -73,16 +106,24 @@ describe('Rollup Plugin', () => {
const myPkg = uniq('my-pkg');
runCLI(`generate @nx/js:lib ${myPkg} --bundler=none`);
runCLI(
`generate @nx/rollup:configuration ${myPkg} --target=node --tsConfig=libs/${myPkg}/tsconfig.lib.json --main=libs/${myPkg}/src/index.ts --compiler=tsc`
`generate @nx/rollup:configuration ${myPkg} --tsConfig=./tsconfig.lib.json --main=./src/index.ts --compiler=tsc`
);
updateJson(join('libs', myPkg, 'project.json'), (config) => {
config.targets.build.options.format = ['cjs', 'esm'];
config.targets.build.options.generateExportsField = true;
config.targets.build.options.additionalEntryPoints = [
`libs/${myPkg}/src/{foo,bar}.ts`,
];
return config;
});
updateFile(
`libs/${myPkg}/rollup.config.js`,
`
const { withNx } = require('@nx/rollup/with-nx');
module.exports = withNx({
outputPath: '../../dist/libs/${myPkg}',
main: './src/index.ts',
tsConfig: './tsconfig.lib.json',
compiler: 'tsc',
generateExportsField: true,
additionalEntryPoints: ['./src/{foo,bar}.ts'],
format: ['cjs', 'esm']
});
`
);
updateFile(`libs/${myPkg}/src/foo.ts`, `export const foo = 'foo';`);
updateFile(`libs/${myPkg}/src/bar.ts`, `export const bar = 'bar';`);
@ -121,49 +162,7 @@ describe('Rollup Plugin', () => {
expect(() => runCLI(`build ${jsLib}`)).not.toThrow();
});
it('should be able to build libs generated with @nx/js:lib --bundler rollup with a custom rollup.config.{cjs|mjs}', () => {
const jsLib = uniq('jslib');
runCLI(`generate @nx/js:lib ${jsLib} --bundler rollup`);
updateFile(
`libs/${jsLib}/rollup.config.cjs`,
`module.exports = {
output: {
format: "cjs",
dir: "dist/test",
name: "Mylib",
entryFileNames: "[name].cjs.js",
chunkFileNames: "[name].cjs.js"
}
}`
);
updateJson(join('libs', jsLib, 'project.json'), (config) => {
config.targets.build.options.rollupConfig = `libs/${jsLib}/rollup.config.cjs`;
return config;
});
expect(() => runCLI(`build ${jsLib}`)).not.toThrow();
checkFilesExist(`dist/test/index.cjs.js`);
updateFile(
`libs/${jsLib}/rollup.config.mjs`,
`export default {
output: {
format: "es",
dir: "dist/test",
name: "Mylib",
entryFileNames: "[name].mjs.js",
chunkFileNames: "[name].mjs.js"
}
}`
);
updateJson(join('libs', jsLib, 'project.json'), (config) => {
config.targets.build.options.rollupConfig = `libs/${jsLib}/rollup.config.mjs`;
return config;
});
expect(() => runCLI(`build ${jsLib}`)).not.toThrow();
checkFilesExist(`dist/test/index.mjs.js`);
});
it('should build correctly with crystal', () => {
it('should work correctly with custom, non-Nx rollup config', () => {
// ARRANGE
packageInstall('@rollup/plugin-babel', undefined, '5.3.0', 'prod');
packageInstall('@rollup/plugin-commonjs', undefined, '25.0.7', 'prod');
@ -216,30 +215,4 @@ export default config;
checkFilesExist(`libs/test/dist/bundle.js`);
checkFilesExist(`libs/test/dist/bundle.es.js`);
});
it('should support array config from rollup.config.js', () => {
const jsLib = uniq('jslib');
runCLI(`generate @nx/js:lib ${jsLib} --bundler rollup --verbose`);
updateFile(
`libs/${jsLib}/rollup.config.js`,
`module.exports = (config) => [{
...config,
output: {
format: "esm",
dir: "dist/test",
name: "Mylib",
entryFileNames: "[name].js",
chunkFileNames: "[name].js"
}
}]`
);
updateJson(join('libs', jsLib, 'project.json'), (config) => {
config.targets.build.options.rollupConfig = `libs/${jsLib}/rollup.config.js`;
return config;
});
expect(() => runCLI(`build ${jsLib} --format=esm`)).not.toThrow();
checkFilesExist(`dist/test/index.js`);
});
});

View File

@ -58,6 +58,7 @@ interface LintProjectOptions {
* @internal
*/
addExplicitTargets?: boolean;
addPackageJsonDependencyChecks?: boolean;
}
export function lintProjectGenerator(tree: Tree, options: LintProjectOptions) {
@ -158,6 +159,7 @@ export async function lintProjectGeneratorInternal(
if (!options.rootProject || projectConfig.root !== '.') {
createEsLintConfiguration(
tree,
options,
projectConfig,
options.setParserOptionsProject,
options.rootProject
@ -188,6 +190,7 @@ export async function lintProjectGeneratorInternal(
function createEsLintConfiguration(
tree: Tree,
options: LintProjectOptions,
projectConfig: ProjectConfiguration,
setParserOptionsProject: boolean,
rootProject: boolean
@ -236,7 +239,10 @@ function createEsLintConfiguration(
},
];
if (isBuildableLibraryProject(projectConfig)) {
if (
options.addPackageJsonDependencyChecks ||
isBuildableLibraryProject(projectConfig)
) {
overrides.push({
files: ['*.json'],
parser: 'jsonc-eslint-parser',

View File

@ -974,11 +974,6 @@ describe('lib', () => {
projectNameAndRootFormat: 'as-provided',
});
const config = readProjectConfiguration(tree, 'my-lib');
expect(config.targets.build.options.project).toEqual(
`my-lib/package.json`
);
const pkgJson = readJson(tree, 'my-lib/package.json');
expect(pkgJson.type).not.toBeDefined();
});
@ -990,9 +985,6 @@ describe('lib', () => {
bundler: 'rollup',
projectNameAndRootFormat: 'as-provided',
});
const config = readProjectConfiguration(tree, 'my-lib');
expect(config.targets.build.options.compiler).toEqual('swc');
});
});
@ -1509,10 +1501,6 @@ describe('lib', () => {
projectNameAndRootFormat: 'as-provided',
});
const project = readProjectConfiguration(tree, 'my-lib');
expect(project.targets.build).toMatchObject({
executor: '@nx/rollup:rollup',
});
expect(readJson(tree, 'my-lib/.eslintrc.json').overrides).toContainEqual({
files: ['*.json'],
parser: 'jsonc-eslint-parser',

View File

@ -89,6 +89,15 @@ export async function libraryGeneratorInternal(
tasks.push(await setupVerdaccio(tree, { ...options, skipFormat: true }));
}
if (options.bundler === 'rollup') {
const { configurationGenerator } = ensurePackage('@nx/rollup', nxVersion);
await configurationGenerator(tree, {
project: options.name,
compiler: 'swc',
format: ['cjs', 'esm'],
});
}
if (options.bundler === 'vite') {
const { viteConfigurationGenerator, createOrEditViteConfig } =
ensurePackage('@nx/vite', nxVersion);
@ -207,47 +216,38 @@ async function addProject(tree: Tree, options: NormalizedSchema) {
options.bundler !== 'none' &&
options.config !== 'npm-scripts'
) {
const outputPath = getOutputPath(options);
if (options.bundler !== 'rollup') {
const outputPath = getOutputPath(options);
const executor = getBuildExecutor(options.bundler);
addBuildTargetDefaults(tree, executor);
const executor = getBuildExecutor(options.bundler);
projectConfiguration.targets.build = {
executor,
outputs: ['{options.outputPath}'],
options: {
outputPath,
main:
`${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'),
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
assets: [],
},
};
addBuildTargetDefaults(tree, executor);
if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.generatePackageJson = true;
projectConfiguration.targets.build.options.format = ['cjs'];
}
projectConfiguration.targets.build = {
executor,
outputs: ['{options.outputPath}'],
options: {
outputPath,
main: `${options.projectRoot}/src/index` + (options.js ? '.js' : '.ts'),
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
assets: [],
},
};
if (options.bundler === 'swc' && options.skipTypeCheck) {
projectConfiguration.targets.build.options.skipTypeCheck = true;
}
if (options.bundler === 'esbuild') {
projectConfiguration.targets.build.options.generatePackageJson = true;
projectConfiguration.targets.build.options.format = ['cjs'];
}
if (options.bundler === 'rollup') {
projectConfiguration.targets.build.options.project = `${options.projectRoot}/package.json`;
projectConfiguration.targets.build.options.compiler = 'swc';
projectConfiguration.targets.build.options.format = ['cjs', 'esm'];
}
if (options.bundler === 'swc' && options.skipTypeCheck) {
projectConfiguration.targets.build.options.skipTypeCheck = true;
}
if (
!options.minimal &&
// TODO(jack): assets for rollup have validation that we need to fix (assets must be under <project-root>/src)
options.bundler !== 'rollup'
) {
projectConfiguration.targets.build.options.assets ??= [];
projectConfiguration.targets.build.options.assets.push(
joinPathFragments(options.projectRoot, '*.md')
);
if (!options.minimal) {
projectConfiguration.targets.build.options.assets ??= [];
projectConfiguration.targets.build.options.assets.push(
joinPathFragments(options.projectRoot, '*.md')
);
}
}
if (options.publishable) {
@ -321,6 +321,8 @@ export async function addLint(
setParserOptionsProject: options.setParserOptionsProject,
rootProject: options.rootProject,
addPlugin: options.addPlugin,
// Since the build target is inferred now, we need to let the generator know to add @nx/dependency-checks regardless.
addPackageJsonDependencyChecks: options.bundler !== 'none',
});
const {
addOverrideToLintConfig,

View File

@ -138,19 +138,7 @@ describe('setup-build generator', () => {
bundler: 'rollup',
});
const config = readProjectConfiguration(tree, 'mypkg');
expect(config).toMatchObject({
targets: {
build: {
executor: '@nx/rollup:rollup',
options: {
outputPath: 'dist/packages/mypkg',
main: 'packages/mypkg/src/main.ts',
tsConfig: 'packages/mypkg/tsconfig.lib.json',
},
},
},
});
expect(tree.exists('packages/mypkg/rollup.config.js')).toBe(true);
});
it('should support --bundler=esbuild', async () => {

View File

@ -16,10 +16,7 @@ import { stripIndents } from '@nx/devkit';
* We want a third file: `dist/index.d.ts` that re-exports from `src/index.d.ts`.
* That way, when TSC or IDEs look for types, it will find them in the right place.
*/
export function typeDefinitions(options: {
projectRoot: string;
main: string;
}) {
export function typeDefinitions(options: { projectRoot: string }) {
return {
name: 'dts-bundle',
async generateBundle(_opts: unknown, bundle: OutputBundle): Promise<void> {

View File

@ -1,11 +1,14 @@
import { Tree } from 'nx/src/generators/tree';
import {
GeneratorCallback,
addDependenciesToPackageJson,
ensurePackage,
GeneratorCallback,
joinPathFragments,
offsetFromRoot,
readNxJson,
readProjectConfiguration,
runTasksInSerial,
stripIndents,
updateProjectConfiguration,
} from '@nx/devkit';
@ -49,8 +52,6 @@ export async function addRollupBuildTarget(
);
}
const { targets } = readProjectConfiguration(host, options.name);
const external: string[] = ['react', 'react-dom'];
if (options.style === '@emotion/styled') {
@ -59,34 +60,80 @@ export async function addRollupBuildTarget(
external.push('react/jsx-runtime');
}
targets.build = {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
outputPath: joinPathFragments('dist', options.projectRoot),
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
project: `${options.projectRoot}/package.json`,
entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`),
external,
rollupConfig: `@nx/react/plugins/bundle-rollup`,
compiler: options.compiler ?? 'babel',
assets: [
{
glob: `${options.projectRoot}/README.md`,
input: '.',
output: '.',
},
],
},
};
const nxJson = readNxJson(host);
const hasRollupPlugin = !!nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/rollup/plugin'
: p.plugin === '@nx/rollup/plugin'
);
if (hasRollupPlugin) {
// New behavior, using rollup config file and inferred target.
host.write(
joinPathFragments(options.projectRoot, 'rollup.config.js'),
stripIndents`
const { withNx } = require('@nx/rollup/with-nx');
const url = require('@rollup/plugin-url');
const svg = require('@svgr/rollup');
module.exports = withNx({
main: '${maybeJs(options, './src/index.ts')}',
outputPath: '${joinPathFragments(
offsetFromRoot(options.projectRoot),
'dist',
options.projectRoot
)}',
tsConfig: './tsconfig.lib.json',
compiler: '${options.compiler ?? 'babel'}',
external: ${JSON.stringify(external)},
format: ['esm'],
assets:[{ input: '.', output: '.', glob: 'README.md'}],
}, {
// Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
plugins: [
svg({
svgo: false,
titleProp: true,
ref: true,
}),
url({
limit: 10000, // 10kB
}),
],
});
`
);
} else {
// Legacy behavior, there is a target in project.json using rollup executor.
const { targets } = readProjectConfiguration(host, options.name);
targets.build = {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
outputPath: joinPathFragments('dist', options.projectRoot),
tsConfig: `${options.projectRoot}/tsconfig.lib.json`,
project: `${options.projectRoot}/package.json`,
entryFile: maybeJs(options, `${options.projectRoot}/src/index.ts`),
external,
rollupConfig: `@nx/react/plugins/bundle-rollup`,
compiler: options.compiler ?? 'babel',
assets: [
{
glob: `${options.projectRoot}/README.md`,
input: '.',
output: '.',
},
],
},
};
updateProjectConfiguration(host, options.name, {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'library',
tags: options.parsedTags,
targets,
});
updateProjectConfiguration(host, options.name, {
root: options.projectRoot,
sourceRoot: joinPathFragments(options.projectRoot, 'src'),
projectType: 'library',
tags: options.parsedTags,
targets,
});
}
return runTasksInSerial(...tasks);
}

View File

@ -496,25 +496,93 @@ describe('lib', () => {
});
describe('--buildable', () => {
it('should have a builder defined', async () => {
it('should default to rollup bundler', async () => {
await libraryGenerator(tree, {
...defaultSchema,
buildable: true,
});
const projectsConfigurations = getProjects(tree);
expect(projectsConfigurations.get('my-lib').targets.build).toBeDefined();
expect(tree.exists('my-lib/rollup.config.js')).toBeTruthy();
});
});
describe('--publishable', () => {
it('should add build targets', async () => {
it('should fail if no importPath is provided with publishable', async () => {
expect.assertions(1);
try {
await libraryGenerator(tree, {
...defaultSchema,
directory: 'myDir',
publishable: true,
});
} catch (e) {
expect(e.message).toContain(
'For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)'
);
}
});
it('should add package.json and .babelrc', async () => {
await libraryGenerator(tree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
});
const packageJson = readJson(tree, '/my-lib/package.json');
expect(packageJson.name).toEqual('@proj/my-lib');
expect(tree.exists('/my-lib/.babelrc'));
});
it('should add rollup config file', async () => {
await libraryGenerator(tree, {
...defaultSchema,
skipFormat: false,
publishable: true,
importPath: '@proj/my-lib',
});
expect(tree.read('my-lib/rollup.config.js', 'utf-8'))
.toEqual(`const { withNx } = require('@nx/rollup/with-nx');
const url = require('@rollup/plugin-url');
const svg = require('@svgr/rollup');
module.exports = withNx(
{
main: './src/index.ts',
outputPath: '../dist/my-lib',
tsConfig: './tsconfig.lib.json',
compiler: 'babel',
external: ['react', 'react-dom', 'react/jsx-runtime'],
format: ['esm'],
assets: [{ input: '.', output: '.', glob: 'README.md' }],
},
{
// Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
plugins: [
svg({
svgo: false,
titleProp: true,
ref: true,
}),
url({
limit: 10000, // 10kB
}),
],
}
);
`);
});
it('should add build targets (legacy)', async () => {
await libraryGenerator(tree, {
...defaultSchema,
addPlugin: false,
publishable: true,
importPath: '@proj/my-lib',
});
const projectsConfigurations = getProjects(tree);
expect(projectsConfigurations.get('my-lib').targets.build).toMatchObject({
@ -531,25 +599,10 @@ describe('lib', () => {
});
});
it('should fail if no importPath is provided with publishable', async () => {
expect.assertions(1);
try {
await libraryGenerator(tree, {
...defaultSchema,
directory: 'myDir',
publishable: true,
});
} catch (e) {
expect(e.message).toContain(
'For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)'
);
}
});
it('should support styled-components', async () => {
it('should support styled-components (legacy)', async () => {
await libraryGenerator(tree, {
...defaultSchema,
addPlugin: false,
publishable: true,
importPath: '@proj/my-lib',
style: 'styled-components',
@ -568,9 +621,10 @@ describe('lib', () => {
]);
});
it('should support @emotion/styled', async () => {
it('should support @emotion/styled (legacy)', async () => {
await libraryGenerator(tree, {
...defaultSchema,
addPlugin: false,
publishable: true,
importPath: '@proj/my-lib',
style: '@emotion/styled',
@ -591,9 +645,10 @@ describe('lib', () => {
);
});
it('should support styled-jsx', async () => {
it('should support styled-jsx (legacy)', async () => {
await libraryGenerator(tree, {
...defaultSchema,
addPlugin: false,
publishable: true,
importPath: '@proj/my-lib',
style: 'styled-jsx',
@ -610,9 +665,10 @@ describe('lib', () => {
expect(babelrc.plugins).toEqual(['styled-jsx/babel']);
});
it('should support style none', async () => {
it('should support style none (legacy)', async () => {
await libraryGenerator(tree, {
...defaultSchema,
addPlugin: false,
publishable: true,
importPath: '@proj/my-lib',
style: 'none',
@ -626,18 +682,6 @@ describe('lib', () => {
},
});
});
it('should add package.json and .babelrc', async () => {
await libraryGenerator(tree, {
...defaultSchema,
publishable: true,
importPath: '@proj/my-lib',
});
const packageJson = readJson(tree, '/my-lib/package.json');
expect(packageJson.name).toEqual('@proj/my-lib');
expect(tree.exists('/my-lib/.babelrc'));
});
});
describe('--js', () => {

View File

@ -4,7 +4,6 @@ import { RollupExecutorOptions } from '../schema';
describe('normalizeRollupExecutorOptions', () => {
let testOptions: RollupExecutorOptions;
let root: string;
let sourceRoot: string;
beforeEach(() => {
testOptions = {
@ -16,15 +15,16 @@ describe('normalizeRollupExecutorOptions', () => {
format: ['esm'],
};
root = '/root';
sourceRoot = 'apps/nodeapp/src';
});
it('should resolve both node modules and relative path for rollupConfig', () => {
let result = normalizeRollupExecutorOptions(
testOptions,
{ root } as any,
sourceRoot
);
let result = normalizeRollupExecutorOptions(testOptions, {
root,
projectGraph: {
nodes: { nodeapp: { data: { root: 'apps/nodeapp' } } },
},
projectName: 'nodeapp',
} as any);
expect(result.rollupConfig).toEqual(['/root/apps/nodeapp/rollup.config']);
result = normalizeRollupExecutorOptions(
@ -33,8 +33,13 @@ describe('normalizeRollupExecutorOptions', () => {
// something that exists in node_modules
rollupConfig: 'react',
},
{ root } as any,
sourceRoot
{
root,
projectGraph: {
nodes: { nodeapp: { data: { root: 'apps/nodeapp' } } },
},
projectName: 'nodeapp',
} as any
);
expect(result.rollupConfig).toHaveLength(1);
expect(result.rollupConfig[0]).toMatch('react');
@ -45,11 +50,13 @@ describe('normalizeRollupExecutorOptions', () => {
it('should handle rollupConfig being undefined', () => {
delete testOptions.rollupConfig;
const result = normalizeRollupExecutorOptions(
testOptions,
{ root } as any,
sourceRoot
);
const result = normalizeRollupExecutorOptions(testOptions, {
root,
projectGraph: {
nodes: { nodeapp: { data: { root: 'apps/nodeapp' } } },
},
projectName: 'nodeapp',
} as any);
expect(result.rollupConfig).toEqual([]);
});
});

View File

@ -1,52 +1,26 @@
import { basename, dirname, join, relative, resolve } from 'path';
import { statSync } from 'fs';
import { ExecutorContext, normalizePath } from '@nx/devkit';
import { resolve } from 'path';
import { ExecutorContext } from '@nx/devkit';
import type { AssetGlobPattern, RollupExecutorOptions } from '../schema';
import { createEntryPoints } from '@nx/js';
import type { RollupExecutorOptions } from '../schema';
export interface NormalizedRollupExecutorOptions extends RollupExecutorOptions {
entryRoot: string;
projectRoot: string;
assets: AssetGlobPattern[];
rollupConfig: string[];
}
export function normalizeRollupExecutorOptions(
options: RollupExecutorOptions,
context: ExecutorContext,
sourceRoot: string
context: ExecutorContext
): NormalizedRollupExecutorOptions {
const { root } = context;
const main = `${root}/${options.main}`;
const entryRoot = dirname(main);
const project = options.project
? `${root}/${options.project}`
: join(root, 'package.json');
const projectRoot = dirname(project);
const outputPath = `${root}/${options.outputPath}`;
return {
...options,
// de-dupe formats
format: Array.from(new Set(options.format)),
rollupConfig: []
.concat(options.rollupConfig)
.filter(Boolean)
.map((p) => normalizePluginPath(p, root)),
assets: options.assets
? normalizeAssets(options.assets, root, sourceRoot)
: undefined,
main,
entryRoot,
project,
projectRoot,
outputPath,
projectRoot: context.projectGraph.nodes[context.projectName].data.root,
skipTypeCheck: options.skipTypeCheck || false,
additionalEntryPoints: createEntryPoints(
options.additionalEntryPoints,
context.root
),
};
}
@ -60,50 +34,3 @@ export function normalizePluginPath(pluginPath: void | string, root: string) {
return resolve(root, pluginPath);
}
}
export function normalizeAssets(
assets: any[],
root: string,
sourceRoot: string
): AssetGlobPattern[] {
return assets.map((asset) => {
if (typeof asset === 'string') {
const assetPath = normalizePath(asset);
const resolvedAssetPath = resolve(root, assetPath);
const resolvedSourceRoot = resolve(root, sourceRoot);
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
);
}
const isDirectory = statSync(resolvedAssetPath).isDirectory();
const input = isDirectory
? resolvedAssetPath
: dirname(resolvedAssetPath);
const output = relative(resolvedSourceRoot, resolve(root, input));
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
return {
input,
output,
glob,
};
} else {
if (asset.output.startsWith('..')) {
throw new Error(
'An asset cannot be written to a location outside of the output path.'
);
}
const assetPath = normalizePath(asset.input);
const resolvedAssetPath = resolve(root, assetPath);
return {
...asset,
input: resolvedAssetPath,
// Now we remove starting slash to make Webpack place it from the output root.
output: asset.output.replace(/^\//, ''),
};
}
});
}

View File

@ -1,85 +1,24 @@
import * as ts from 'typescript';
import * as rollup from 'rollup';
import { getBabelInputPlugin } from '@rollup/plugin-babel';
import { dirname, join, parse, resolve } from 'path';
import * as autoprefixer from 'autoprefixer';
import {
type ExecutorContext,
joinPathFragments,
logger,
names,
readJsonFile,
} from '@nx/devkit';
import {
calculateProjectBuildableDependencies,
computeCompilerOptionsPaths,
DependentBuildableProjectNode,
} from '@nx/js/src/utils/buildable-libs-utils';
import nodeResolve from '@rollup/plugin-node-resolve';
import type { PackageJson } from 'nx/src/utils/package-json';
import { typeDefinitions } from '@nx/js/src/plugins/rollup/type-definitions';
import { parse, resolve } from 'path';
import { type ExecutorContext, logger } from '@nx/devkit';
import { AssetGlobPattern, RollupExecutorOptions } from './schema';
import { RollupExecutorOptions } from './schema';
import {
NormalizedRollupExecutorOptions,
normalizeRollupExecutorOptions,
} from './lib/normalize';
import { analyze } from './lib/analyze-plugin';
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';
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');
const image = require('@rollup/plugin-image');
const json = require('@rollup/plugin-json');
const copy = require('rollup-plugin-copy');
const postcss = require('rollup-plugin-postcss');
const fileExtensions = ['.js', '.jsx', '.ts', '.tsx'];
import { withNx } from '../../plugins/with-nx/with-nx';
import { calculateProjectBuildableDependencies } from '@nx/js/src/utils/buildable-libs-utils';
export async function* rollupExecutor(
rawOptions: RollupExecutorOptions,
context: ExecutorContext
) {
process.env.NODE_ENV ??= 'production';
const project = context.projectsConfigurations.projects[context.projectName];
const sourceRoot = project.sourceRoot;
const { dependencies } = calculateProjectBuildableDependencies(
context.taskGraph,
context.projectGraph,
context.root,
context.projectName,
context.targetName,
context.configurationName,
true
);
const options = normalizeRollupExecutorOptions(
rawOptions,
context,
sourceRoot
);
const packageJson = readJsonFile(options.project);
const npmDeps = (context.projectGraph.dependencies[context.projectName] ?? [])
.filter((d) => d.target.startsWith('npm:'))
.map((d) => d.target.slice(4));
const rollupOptions = await createRollupOptions(
options,
dependencies,
context,
packageJson,
sourceRoot,
npmDeps
);
const options = normalizeRollupExecutorOptions(rawOptions, context);
const rollupOptions = await createRollupOptions(options, context);
const outfile = resolveOutfile(context, options);
if (options.watch) {
@ -90,7 +29,6 @@ export async function* rollupExecutor(
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') {
@ -111,11 +49,6 @@ export async function* rollupExecutor(
try {
logger.info(`Bundling ${context.projectName}...`);
// Delete output path before bundling
if (options.deleteOutputPath) {
deleteOutputDir(context.root, options.outputPath);
}
const start = process.hrtime.bigint();
const allRollupOptions = Array.isArray(rollupOptions)
? rollupOptions
@ -133,7 +66,6 @@ export async function* rollupExecutor(
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 (e) {
@ -154,140 +86,19 @@ export async function* rollupExecutor(
export async function createRollupOptions(
options: NormalizedRollupExecutorOptions,
dependencies: DependentBuildableProjectNode[],
context: ExecutorContext,
packageJson: PackageJson,
sourceRoot: string,
npmDeps: string[]
context: ExecutorContext
): Promise<rollup.RollupOptions | rollup.RollupOptions[]> {
const useBabel = options.compiler === 'babel';
const useSwc = options.compiler === 'swc';
const tsConfigPath = joinPathFragments(context.root, options.tsConfig);
const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
const config = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
dirname(tsConfigPath)
const { dependencies } = calculateProjectBuildableDependencies(
context.taskGraph,
context.projectGraph,
context.root,
context.projectName,
context.targetName,
context.configurationName,
true
);
if (!options.format || !options.format.length) {
options.format = readCompatibleFormats(config);
}
if (packageJson.type === 'module') {
if (options.format.includes('cjs')) {
logger.warn(
`Package type is set to "module" but "cjs" format is included. Going to use "esm" format instead. You can change the package type to "commonjs" or remove type in the package.json file.`
);
}
options.format = ['esm'];
} else if (packageJson.type === 'commonjs') {
if (options.format.includes('esm')) {
logger.warn(
`Package type is set to "commonjs" but "esm" format is included. Going to use "cjs" format instead. You can change the package type to "module" or remove type in the package.json file.`
);
}
options.format = ['cjs'];
}
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,
}),
postcss({
inject: true,
extract: options.extractCss,
autoModules: true,
plugins: [autoprefixer],
use: {
less: {
javascriptEnabled: options.javascriptEnabled,
},
},
}),
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/,
}),
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<string, string> = {};
input[parse(mainEntryFileName).name] = options.main;
options.additionalEntryPoints.forEach((entry) => {
input[parse(entry).name] = entry;
});
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,
};
const rollupConfig = withNx(options, {}, dependencies);
const userDefinedRollupConfigs = options.rollupConfig.map((plugin) =>
loadConfigFile(plugin)
@ -314,57 +125,6 @@ export async function createRollupOptions(
return finalConfig;
}
function createTsCompilerOptions(
config: ts.ParsedCommandLine,
dependencies: DependentBuildableProjectNode[],
options: NormalizedRollupExecutorOptions
) {
const compilerOptionPaths = computeCompilerOptionsPaths(config, dependencies);
const compilerOptions = {
rootDir: options.projectRoot,
allowJs: options.allowJs,
declaration: true,
paths: compilerOptionPaths,
};
if (config.options.module === ts.ModuleKind.CommonJS) {
compilerOptions['module'] = 'ESNext';
}
if (options.compiler === 'swc') {
compilerOptions['emitDeclarationOnly'] = true;
}
return compilerOptions;
}
interface RollupCopyAssetOption {
src: string;
dest: string;
}
function convertCopyAssetsToRollupOptions(
outputPath: string,
assets: AssetGlobPattern[]
): RollupCopyAssetOption[] {
return assets
? assets.map((a) => ({
src: join(a.input, a.glob).replace(/\\/g, '/'),
dest: join(outputPath, a.output).replace(/\\/g, '/'),
}))
: undefined;
}
function readCompatibleFormats(
config: ts.ParsedCommandLine
): ('cjs' | 'esm')[] {
switch (config.options.module) {
case ts.ModuleKind.CommonJS:
case ts.ModuleKind.UMD:
case ts.ModuleKind.AMD:
return ['cjs'];
default:
return ['esm'];
}
}
function resolveOutfile(
context: ExecutorContext,
options: NormalizedRollupExecutorOptions

View File

@ -0,0 +1,148 @@
import 'nx/src/internal-testing-utils/mock-project-graph';
import {
addProjectConfiguration,
readJson,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import configurationGenerator from './configuration';
describe('configurationGenerator', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
addProjectConfiguration(tree, 'mypkg', {
root: 'libs/mypkg',
sourceRoot: 'libs/mypkg/src',
targets: {},
});
});
it('should generate files', async () => {
await configurationGenerator(tree, {
addPlugin: false,
project: 'mypkg',
});
const project = readProjectConfiguration(tree, 'mypkg');
expect(project.targets).toMatchObject({
build: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
main: 'libs/mypkg/src/index.ts',
},
},
});
expect(readJson(tree, 'libs/mypkg/package.json')).toEqual({
name: '@proj/mypkg',
version: '0.0.1',
});
});
it('should respect existing package.json file', async () => {
writeJson(tree, 'libs/mypkg/package.json', {
name: '@acme/mypkg',
version: '1.0.0',
});
await configurationGenerator(tree, {
addPlugin: false,
project: 'mypkg',
});
expect(readJson(tree, 'libs/mypkg/package.json')).toEqual({
name: '@acme/mypkg',
version: '1.0.0',
});
});
it('should support --main option', async () => {
await configurationGenerator(tree, {
addPlugin: false,
project: 'mypkg',
main: 'libs/mypkg/index.ts',
});
const project = readProjectConfiguration(tree, 'mypkg');
expect(project.targets).toMatchObject({
build: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
main: 'libs/mypkg/index.ts',
},
},
});
});
it('should support --tsConfig option', async () => {
await configurationGenerator(tree, {
addPlugin: false,
project: 'mypkg',
tsConfig: 'libs/mypkg/tsconfig.custom.json',
});
const project = readProjectConfiguration(tree, 'mypkg');
expect(project.targets).toMatchObject({
build: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
tsConfig: 'libs/mypkg/tsconfig.custom.json',
},
},
});
});
it('should carry over known executor options from existing build target', async () => {
updateProjectConfiguration(tree, 'mypkg', {
root: 'libs/mypkg',
sourceRoot: 'libs/mypkg/src',
targets: {
build: {
executor: '@nx/js:tsc',
options: {
main: 'libs/mypkg/src/custom.ts',
outputPath: 'dist/custom',
tsConfig: 'libs/mypkg/src/tsconfig.custom.json',
additionalEntryPoints: ['libs/mypkg/src/extra.ts'],
generateExportsField: true,
},
},
},
});
await configurationGenerator(tree, {
addPlugin: false,
project: 'mypkg',
buildTarget: 'build',
skipValidation: true,
});
const project = readProjectConfiguration(tree, 'mypkg');
expect(project.targets).toMatchObject({
build: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
options: {
main: 'libs/mypkg/src/custom.ts',
outputPath: 'dist/custom',
tsConfig: 'libs/mypkg/src/tsconfig.custom.json',
additionalEntryPoints: ['libs/mypkg/src/extra.ts'],
generateExportsField: true,
},
},
});
});
});

View File

@ -5,7 +5,6 @@ import {
readJson,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
@ -31,16 +30,7 @@ describe('configurationGenerator', () => {
const project = readProjectConfiguration(tree, 'mypkg');
expect(project.targets).toMatchObject({
build: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
main: 'libs/mypkg/src/main.ts',
},
},
});
expect(project.targets?.build).toBeUndefined();
expect(readJson(tree, 'libs/mypkg/package.json')).toEqual({
name: '@proj/mypkg',
@ -66,82 +56,58 @@ describe('configurationGenerator', () => {
it('should support --main option', async () => {
await configurationGenerator(tree, {
project: 'mypkg',
main: 'libs/mypkg/index.ts',
main: './src/index.ts',
});
const project = readProjectConfiguration(tree, 'mypkg');
const rollupConfig = tree.read('libs/mypkg/rollup.config.js', 'utf-8');
expect(project.targets).toMatchObject({
build: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
main: 'libs/mypkg/index.ts',
},
},
});
expect(rollupConfig)
.toEqual(`const { withNx } = require('@nx/rollup/with-nx');
module.exports = withNx(
{
main: './src/index.ts',
outputPath: '../../dist/libs/mypkg',
tsConfig: './tsconfig.lib.json',
compiler: 'babel',
format: ['esm'],
assets: [{ input: '.', output: '.', glob: '*.md' }],
},
{
// Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
// e.g.
// output: { sourcemap: true },
}
);
`);
});
it('should support --tsConfig option', async () => {
await configurationGenerator(tree, {
project: 'mypkg',
tsConfig: 'libs/mypkg/tsconfig.custom.json',
tsConfig: './tsconfig.custom.json',
});
const project = readProjectConfiguration(tree, 'mypkg');
const rollupConfig = tree.read('libs/mypkg/rollup.config.js', 'utf-8');
expect(project.targets).toMatchObject({
build: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
tsConfig: 'libs/mypkg/tsconfig.custom.json',
},
},
});
});
expect(rollupConfig)
.toEqual(`const { withNx } = require('@nx/rollup/with-nx');
it('should carry over known executor options from existing build target', async () => {
updateProjectConfiguration(tree, 'mypkg', {
root: 'libs/mypkg',
sourceRoot: 'libs/mypkg/src',
targets: {
build: {
executor: '@nx/js:tsc',
options: {
main: 'libs/mypkg/src/custom.ts',
outputPath: 'dist/custom',
tsConfig: 'libs/mypkg/src/tsconfig.custom.json',
additionalEntryPoints: ['libs/mypkg/src/extra.ts'],
generateExportsField: true,
},
},
},
});
await configurationGenerator(tree, {
project: 'mypkg',
buildTarget: 'build',
skipValidation: true,
});
const project = readProjectConfiguration(tree, 'mypkg');
expect(project.targets).toMatchObject({
build: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
main: 'libs/mypkg/src/custom.ts',
outputPath: 'dist/custom',
tsConfig: 'libs/mypkg/src/tsconfig.custom.json',
additionalEntryPoints: ['libs/mypkg/src/extra.ts'],
generateExportsField: true,
},
},
});
module.exports = withNx(
{
main: './src/index.ts',
outputPath: '../../dist/libs/mypkg',
tsConfig: './tsconfig.custom.json',
compiler: 'babel',
format: ['esm'],
assets: [{ input: '.', output: '.', glob: '*.md' }],
},
{
// Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
// e.g.
// output: { sourcemap: true },
}
);
`);
});
});

View File

@ -1,10 +1,13 @@
import {
formatFiles,
joinPathFragments,
readProjectConfiguration,
Tree,
GeneratorCallback,
joinPathFragments,
offsetFromRoot,
readNxJson,
readProjectConfiguration,
runTasksInSerial,
stripIndents,
Tree,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
@ -15,20 +18,35 @@ import { RollupExecutorOptions } from '../../executors/rollup/schema';
import { RollupProjectSchema } from './schema';
import { addBuildTargetDefaults } from '@nx/devkit/src/generators/add-build-target-defaults';
import { ensureDependencies } from '../../utils/ensure-dependencies';
import { hasPlugin } from '../../utils/has-plugin';
import { RollupWithNxPluginOptions } from '../../plugins/with-nx/with-nx-options';
export async function configurationGenerator(
tree: Tree,
options: RollupProjectSchema
) {
const tasks: GeneratorCallback[] = [];
const nxJson = readNxJson(tree);
const addPluginDefault =
process.env.NX_ADD_PLUGINS !== 'false' &&
nxJson.useInferencePlugins !== false;
options.addPlugin ??= addPluginDefault;
tasks.push(await rollupInitGenerator(tree, { ...options, skipFormat: true }));
if (!options.skipPackageJson) {
tasks.push(ensureDependencies(tree, options));
}
options.buildTarget ??= 'build';
checkForTargetConflicts(tree, options);
addBuildTarget(tree, options);
if (hasPlugin(tree)) {
createRollupConfig(tree, options);
} else {
options.buildTarget ??= 'build';
checkForTargetConflicts(tree, options);
addBuildTarget(tree, options);
}
addPackageJson(tree, options);
if (!options.skipFormat) {
await formatFiles(tree);
@ -37,6 +55,39 @@ export async function configurationGenerator(
return runTasksInSerial(...tasks);
}
function createRollupConfig(tree: Tree, options: RollupProjectSchema) {
const project = readProjectConfiguration(tree, options.project);
const buildOptions: RollupWithNxPluginOptions = {
outputPath: joinPathFragments(
offsetFromRoot(project.root),
'dist',
project.root === '.' ? project.name : project.root
),
compiler: options.compiler ?? 'babel',
main: options.main ?? './src/index.ts',
tsConfig: options.tsConfig ?? './tsconfig.lib.json',
};
tree.write(
joinPathFragments(project.root, 'rollup.config.js'),
stripIndents`
const { withNx } = require('@nx/rollup/with-nx');
module.exports = withNx({
main: '${buildOptions.main}',
outputPath: '${buildOptions.outputPath}',
tsConfig: '${buildOptions.tsConfig}',
compiler: '${buildOptions.compiler}',
format: ${JSON.stringify(options.format ?? ['esm'])},
assets:[{ input: '.', output: '.', glob:'*.md'}],
}, {
// Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
// e.g.
// output: { sourcemap: true },
});`
);
}
function checkForTargetConflicts(tree: Tree, options: RollupProjectSchema) {
if (options.skipValidation) return;
const project = readProjectConfiguration(tree, options.project);
@ -47,8 +98,7 @@ function checkForTargetConflicts(tree: Tree, options: RollupProjectSchema) {
}
}
function addBuildTarget(tree: Tree, options: RollupProjectSchema) {
addBuildTargetDefaults(tree, '@nx/rollup:rollup', options.buildTarget);
function addPackageJson(tree: Tree, options: RollupProjectSchema) {
const project = readProjectConfiguration(tree, options.project);
const packageJsonPath = joinPathFragments(project.root, 'package.json');
@ -60,14 +110,18 @@ function addBuildTarget(tree: Tree, options: RollupProjectSchema) {
version: '0.0.1',
});
}
}
function addBuildTarget(tree: Tree, options: RollupProjectSchema) {
addBuildTargetDefaults(tree, '@nx/rollup:rollup', options.buildTarget);
const project = readProjectConfiguration(tree, options.project);
const prevBuildOptions = project.targets?.[options.buildTarget]?.options;
const buildOptions: RollupExecutorOptions = {
main:
options.main ??
prevBuildOptions?.main ??
joinPathFragments(project.root, 'src/main.ts'),
joinPathFragments(project.root, 'src/index.ts'),
outputPath:
prevBuildOptions?.outputPath ??
joinPathFragments(
@ -107,17 +161,7 @@ function addBuildTarget(tree: Tree, options: RollupProjectSchema) {
[options.buildTarget]: {
executor: '@nx/rollup:rollup',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: buildOptions,
configurations: {
production: {
optimization: true,
sourceMap: false,
namedChunks: false,
extractLicenses: true,
vendorChunk: false,
},
},
},
},
});

View File

@ -11,4 +11,5 @@ export interface RollupProjectSchema {
rollupConfig?: string;
buildTarget?: string;
format?: ('cjs' | 'esm')[];
addPlugin?: boolean;
}

View File

@ -2,7 +2,7 @@ import 'nx/src/internal-testing-utils/mock-project-graph';
import { Tree, readJson, ProjectGraph } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { nxVersion } from '../../utils/versions';
import { nxVersion, rollupVersion } from '../../utils/versions';
import { rollupInitGenerator } from './init';
@ -32,7 +32,7 @@ describe('rollupInitGenerator', () => {
expect(packageJson).toEqual({
name: expect.any(String),
dependencies: {},
devDependencies: { '@nx/rollup': nxVersion },
devDependencies: { '@nx/rollup': nxVersion, rollup: rollupVersion },
});
});
});

View File

@ -5,25 +5,30 @@ import {
GeneratorCallback,
Tree,
} from '@nx/devkit';
import { nxVersion } from '../../utils/versions';
import { nxVersion, rollupVersion } from '../../utils/versions';
import { Schema } from './schema';
import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin';
import { createNodes } from '../../plugins/plugin';
export async function rollupInitGenerator(tree: Tree, schema: Schema) {
let task: GeneratorCallback = () => {};
schema.addPlugin ??= process.env.NX_ADD_PLUGINS !== 'false';
if (!schema.skipPackageJson) {
const devDependencies = { '@nx/rollup': nxVersion };
if (schema.addPlugin) {
// Ensure user can run Rollup CLI.
devDependencies['rollup'] = rollupVersion;
}
task = addDependenciesToPackageJson(
tree,
{},
{ '@nx/rollup': nxVersion },
devDependencies,
undefined,
schema.keepExistingVersions
);
}
schema.addPlugin ??= process.env.NX_ADD_PLUGINS !== 'false';
if (schema.addPlugin) {
await addPluginV1(
tree,

View File

@ -0,0 +1,14 @@
import type { Plugin } from 'rollup';
import { deleteOutputDir } from '../utils/fs';
export interface DeleteOutputOptions {
dirs: string[];
}
export function deleteOutput(options: DeleteOutputOptions): Plugin {
return {
name: 'rollup-plugin-nx-delete-output',
buildStart: () =>
options.dirs.forEach((dir) => deleteOutputDir(process.cwd(), dir)),
};
}

View File

@ -0,0 +1,25 @@
import type { Plugin } from 'rollup';
import type { PackageJson } from 'nx/src/utils/package-json';
import { updatePackageJson } from './update-package-json';
export interface GeneratePackageJsonOptions {
outputPath: string;
main: string;
format: string[];
generateExportsField?: boolean;
skipTypeField?: boolean;
outputFileName?: string;
additionalEntryPoints?: string[];
}
export function generatePackageJson(
options: GeneratePackageJsonOptions,
packageJson: PackageJson
): Plugin {
return {
name: 'rollup-plugin-nx-generate-package-json',
writeBundle: () => {
updatePackageJson(options, packageJson);
},
};
}

View File

@ -2,12 +2,18 @@ import { basename, join, parse } from 'path';
import { writeJsonFile } from 'nx/src/utils/fileutils';
import { writeFileSync } from 'fs';
import { PackageJson } from 'nx/src/utils/package-json';
import { NormalizedRollupExecutorOptions } from './normalize';
import { stripIndents } from '@nx/devkit';
import { stripIndents, workspaceRoot } from '@nx/devkit';
// TODO(jack): Use updatePackageJson from @nx/js instead.
export function updatePackageJson(
options: NormalizedRollupExecutorOptions,
options: {
outputPath: string;
main: string;
format: string[];
generateExportsField?: boolean;
skipTypeField?: boolean;
outputFileName?: string;
additionalEntryPoints?: string[];
},
packageJson: PackageJson
) {
const hasEsmFormat = options.format.includes('esm');
@ -72,13 +78,14 @@ export function updatePackageJson(
// default import in Node will not work.
writeFileSync(
join(
workspaceRoot,
options.outputPath,
filePath.replace(/\.cjs\.js$/, '.cjs.default.js')
),
`exports._default = require('./${parse(filePath).base}').default;`
);
writeFileSync(
join(options.outputPath, fauxEsmFilePath),
join(workspaceRoot, options.outputPath, fauxEsmFilePath),
// Re-export from relative CJS file, and Node will synthetically export it as ESM.
stripIndents`
export * from './${relativeFile}';
@ -95,29 +102,31 @@ export function updatePackageJson(
}
}
writeJsonFile(`${options.outputPath}/package.json`, packageJson);
writeJsonFile(
join(workspaceRoot, options.outputPath, 'package.json'),
packageJson
);
}
interface Exports {
'.': string;
[name: string]: string;
}
function getExports(
options: Pick<
NormalizedRollupExecutorOptions,
'main' | 'projectRoot' | 'outputFileName' | 'additionalEntryPoints'
> & {
fileExt: string;
function getExports(options: {
main?: string;
fileExt: string;
outputFileName?: string;
additionalEntryPoints?: string[];
}): Exports {
const exports: Exports = {};
// Users may provide custom input option and skip the main field.
if (options.main) {
const mainFile = options.outputFileName
? options.outputFileName.replace(/\.[tj]s$/, '')
: basename(options.main).replace(/\.[tj]s$/, '');
exports['.'] = './' + mainFile + options.fileExt;
}
): Exports {
const mainFile = options.outputFileName
? options.outputFileName.replace(/\.[tj]s$/, '')
: basename(options.main).replace(/\.[tj]s$/, '');
const exports: Exports = {
'.': './' + mainFile + options.fileExt,
};
if (options.additionalEntryPoints) {
for (const file of options.additionalEntryPoints) {

View File

@ -0,0 +1,18 @@
import { ProjectGraphProjectNode, readCachedProjectGraph } from '@nx/devkit';
export function getProjectNode(): ProjectGraphProjectNode {
// During graph construction, project is not necessary. Return a stub.
if (global.NX_GRAPH_CREATION) {
return {
type: 'lib',
name: '',
data: {
root: '',
},
};
} else {
const projectGraph = readCachedProjectGraph();
const projectName = process.env.NX_TASK_TARGET_PROJECT;
return projectGraph.nodes[projectName];
}
}

View File

@ -0,0 +1,85 @@
import { normalizeOptions } from './normalize-options';
jest.mock('@nx/js', () => ({
...jest.requireActual('@nx/js'),
createEntryPoints: (x: string) => x,
}));
jest.mock('@nx/devkit', () => ({
...jest.requireActual('@nx/devkit'),
workspaceRoot: '/tmp',
}));
jest.mock('node:fs', () => ({
statSync: () => ({ isDirectory: () => true }),
}));
describe('normalizeOptions', () => {
it('should provide defaults', () => {
const result = normalizeOptions('pkg', 'pkg', {
main: './src/main.ts',
tsConfig: './tsconfig.json',
outputPath: '../dist/pkg',
});
expect(result).toMatchObject({
allowJs: false,
assets: [],
babelUpwardRootMode: false,
compiler: 'babel',
deleteOutputPath: true,
extractCss: true,
generateExportsField: false,
javascriptEnabled: false,
skipTypeCheck: false,
skipTypeField: false,
});
});
it('should normalize relative paths', () => {
const result = normalizeOptions('pkg', 'pkg', {
main: './src/main.ts',
additionalEntryPoints: ['./src/worker1.ts', './src/worker2.ts'],
tsConfig: './tsconfig.json',
outputPath: '../dist/pkg',
});
expect(result).toMatchObject({
additionalEntryPoints: ['pkg/src/worker1.ts', 'pkg/src/worker2.ts'],
main: 'pkg/src/main.ts',
outputPath: 'dist/pkg',
tsConfig: 'pkg/tsconfig.json',
});
});
it('should normalize relative paths', () => {
const result = normalizeOptions('pkg', 'pkg', {
main: './src/main.ts',
tsConfig: './tsconfig.json',
outputPath: '../dist/pkg',
assets: [
'./src/assets',
{ input: './docs', output: '.', glob: '**/*.md' },
],
});
expect(result).toMatchObject({
assets: [
{
glob: '**/*',
input: '/tmp/pkg/src/assets',
output: 'src/assets',
},
{
glob: '**/*.md',
input: '/tmp/docs',
output: '.',
},
],
compiler: 'babel',
main: 'pkg/src/main.ts',
outputPath: 'dist/pkg',
tsConfig: 'pkg/tsconfig.json',
});
});
});

View File

@ -0,0 +1,107 @@
import { basename, dirname, join, relative, resolve } from 'node:path';
import { statSync } from 'node:fs';
import { normalizePath, workspaceRoot } from '@nx/devkit';
import type {
AssetGlobPattern,
NormalizedRollupWithNxPluginOptions,
RollupWithNxPluginOptions,
} from './with-nx-options';
import { createEntryPoints } from '@nx/js';
export function normalizeOptions(
projectRoot: string,
sourceRoot: string,
options: RollupWithNxPluginOptions
): NormalizedRollupWithNxPluginOptions {
if (global.NX_GRAPH_CREATION)
return options as NormalizedRollupWithNxPluginOptions;
normalizeRelativePaths(projectRoot, options);
return {
...options,
additionalEntryPoints: createEntryPoints(
options.additionalEntryPoints,
workspaceRoot
),
allowJs: options.allowJs ?? false,
assets: options.assets
? normalizeAssets(options.assets, workspaceRoot, sourceRoot)
: [],
babelUpwardRootMode: options.babelUpwardRootMode ?? false,
compiler: options.compiler ?? 'babel',
deleteOutputPath: options.deleteOutputPath ?? true,
extractCss: options.extractCss ?? true,
format: options.format ? Array.from(new Set(options.format)) : undefined,
generateExportsField: options.generateExportsField ?? false,
javascriptEnabled: options.javascriptEnabled ?? false,
skipTypeCheck: options.skipTypeCheck ?? false,
skipTypeField: options.skipTypeField ?? false,
};
}
function normalizeAssets(
assets: any[],
root: string,
sourceRoot: string
): AssetGlobPattern[] {
return assets.map((asset) => {
if (typeof asset === 'string') {
const assetPath = normalizePath(asset);
const resolvedAssetPath = resolve(root, assetPath);
const resolvedSourceRoot = resolve(root, sourceRoot);
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
);
}
const isDirectory = statSync(resolvedAssetPath).isDirectory();
const input = isDirectory
? resolvedAssetPath
: dirname(resolvedAssetPath);
const output = relative(resolvedSourceRoot, resolve(root, input));
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
return {
input,
output,
glob,
};
} else {
if (asset.output.startsWith('..')) {
throw new Error(
'An asset cannot be written to a location outside of the output path.'
);
}
const assetPath = normalizePath(asset.input);
const resolvedAssetPath = resolve(root, assetPath);
return {
...asset,
input: resolvedAssetPath,
// Now we remove starting slash to make Webpack place it from the output root.
output: asset.output.replace(/^\//, ''),
};
}
});
}
function normalizeRelativePaths(
projectRoot: string,
options: RollupWithNxPluginOptions
): void {
for (const [fieldName, fieldValue] of Object.entries(options)) {
if (isRelativePath(fieldValue)) {
options[fieldName] = join(projectRoot, fieldValue);
} else if (Array.isArray(fieldValue)) {
for (let i = 0; i < fieldValue.length; i++) {
if (isRelativePath(fieldValue[i])) {
fieldValue[i] = join(projectRoot, fieldValue[i]);
}
}
}
}
}
function isRelativePath(val: unknown): boolean {
return typeof val === 'string' && val.startsWith('.');
}

View File

@ -0,0 +1,37 @@
// TODO: Add TSDoc
export interface RollupWithNxPluginOptions {
additionalEntryPoints?: string[];
allowJs?: boolean;
assets?: any[];
babelUpwardRootMode?: boolean;
compiler?: 'babel' | 'tsc' | 'swc';
deleteOutputPath?: boolean;
external?: string[] | 'all' | 'none';
extractCss?: boolean | string;
format?: ('cjs' | 'esm')[];
generateExportsField?: boolean;
javascriptEnabled?: boolean;
main: string;
/** @deprecated Do not set this. The package.json file in project root is detected automatically. */
project?: string;
outputFileName?: string;
outputPath: string;
rollupConfig?: string | string[];
skipTypeCheck?: boolean;
skipTypeField?: boolean;
tsConfig: string;
}
export interface AssetGlobPattern {
glob: string;
ignore?: string[];
input: string;
output: string;
}
export interface NormalizedRollupWithNxPluginOptions
extends RollupWithNxPluginOptions {
assets: AssetGlobPattern[];
compiler: 'babel' | 'tsc' | 'swc';
format: ('cjs' | 'esm')[];
}

View File

@ -0,0 +1,333 @@
import { existsSync } from 'node:fs';
import { dirname, join, parse } from 'node:path';
import * as ts from 'typescript';
import * as rollup from 'rollup';
import { getBabelInputPlugin } from '@rollup/plugin-babel';
import * as autoprefixer from 'autoprefixer';
import {
joinPathFragments,
logger,
type ProjectGraph,
readCachedProjectGraph,
readJsonFile,
workspaceRoot,
} from '@nx/devkit';
import {
calculateProjectBuildableDependencies,
computeCompilerOptionsPaths,
DependentBuildableProjectNode,
} from '@nx/js/src/utils/buildable-libs-utils';
import nodeResolve from '@rollup/plugin-node-resolve';
import { typeDefinitions } from '@nx/js/src/plugins/rollup/type-definitions';
import { analyze } from '../analyze';
import { swc } from '../swc';
import { generatePackageJson } from '../package-json/generate-package-json';
import { getProjectNode } from './get-project-node';
import { deleteOutput } from '../delete-output';
import { AssetGlobPattern, RollupWithNxPluginOptions } from './with-nx-options';
import { normalizeOptions } from './normalize-options';
import { PackageJson } from 'nx/src/utils/package-json';
// These use require because the ES import isn't correct.
const commonjs = require('@rollup/plugin-commonjs');
const image = require('@rollup/plugin-image');
const json = require('@rollup/plugin-json');
const copy = require('rollup-plugin-copy');
const postcss = require('rollup-plugin-postcss');
const fileExtensions = ['.js', '.jsx', '.ts', '.tsx'];
export function withNx(
rawOptions: RollupWithNxPluginOptions,
rollupConfig: rollup.RollupOptions = {},
// Passed by @nx/rollup:rollup executor to previous behavior of remapping tsconfig paths based on buildable dependencies remains intact.
dependencies?: DependentBuildableProjectNode[]
): rollup.RollupOptions {
const finalConfig: rollup.RollupOptions = { ...rollupConfig };
// Since this is invoked by the executor, the graph has already been created and cached.
const projectNode = getProjectNode();
const projectRoot = join(workspaceRoot, projectNode.data.root);
// Cannot read in graph during construction, but we only need it during build time.
const projectGraph: ProjectGraph | null = global.NX_GRAPH_CREATION
? null
: readCachedProjectGraph();
// If dependencies are not passed from executor, calculate them from project graph.
if (!dependencies && !global.NX_GRAPH_CREATION) {
const result = calculateProjectBuildableDependencies(
undefined,
projectGraph,
workspaceRoot,
projectNode.name,
process.env.NX_TASK_TARGET_TARGET,
process.env.NX_TASK_TARGET_CONFIGURATION,
true
);
dependencies = result.dependencies;
}
const options = normalizeOptions(
projectNode.data.root,
projectNode.data.sourceRoot,
rawOptions
);
const useBabel = options.compiler === 'babel';
const useSwc = options.compiler === 'swc';
const tsConfigPath = joinPathFragments(workspaceRoot, options.tsConfig);
const tsConfigFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
const tsConfig = ts.parseJsonConfigFileContent(
tsConfigFile.config,
ts.sys,
dirname(tsConfigPath)
);
if (!options.format || !options.format.length) {
options.format = readCompatibleFormats(tsConfig);
}
if (
rollupConfig.input &&
(options.main || options.additionalEntryPoints.length > 0)
) {
logger.warn(
`Setting "input" in rollup config overrides "main" and "additionalEntryPoints" options.`
);
}
// If user provides their own input, override our defaults.
finalConfig.input = rollupConfig.input || createInput(options);
if (options.format) {
if (Array.isArray(rollupConfig.output)) {
throw new Error(
`Cannot use Rollup array output option and withNx format option together. Use an object instead.`
);
}
if (rollupConfig.output?.format || rollupConfig.output?.dir) {
logger.warn(
`"output.dir" and "output.format" are overridden by "withNx".`
);
}
finalConfig.output = options.format.map((format) => ({
// These options could be overridden by the user, especially if they use a single format.
entryFileNames: `[name].${format}.js`,
chunkFileNames: `[name].${format}.js`,
...rollupConfig.output,
// Format and dir cannot be overridden by user or else the behavior will break.
format,
dir: global.NX_GRAPH_CREATION
? // Options are not normalized with project root during graph creation due to the lack of project and project root.
// Cannot be joined with workspace root now, but will be handled by @nx/rollup/plugin.
options.outputPath
: join(workspaceRoot, options.outputPath),
}));
}
let packageJson: PackageJson;
if (!global.NX_GRAPH_CREATION) {
const packageJsonPath = options.project
? join(workspaceRoot, options.project)
: join(projectRoot, 'package.json');
if (!existsSync(packageJsonPath)) {
throw new Error(`Cannot find ${packageJsonPath}.`);
}
packageJson = readJsonFile(packageJsonPath);
if (packageJson.type === 'module') {
if (options.format.includes('cjs')) {
logger.warn(
`Package type is set to "module" but "cjs" format is included. Going to use "esm" format instead. You can change the package type to "commonjs" or remove type in the package.json file.`
);
}
options.format = ['esm'];
} else if (packageJson.type === 'commonjs') {
if (options.format.includes('esm')) {
logger.warn(
`Package type is set to "commonjs" but "esm" format is included. Going to use "cjs" format instead. You can change the package type to "module" or remove type in the package.json file.`
);
}
options.format = ['cjs'];
}
}
// User may wish to customize how external behaves by overriding our default.
if (!rollupConfig.external && !global.NX_GRAPH_CREATION) {
const npmDeps = (projectGraph.dependencies[projectNode.name] ?? [])
.filter((d) => d.target.startsWith('npm:'))
.map((d) => d.target.slice(4));
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(npmDeps);
} else if (Array.isArray(options.external) && options.external.length > 0) {
externalPackages = externalPackages.concat(options.external);
}
externalPackages = [...new Set(externalPackages)];
finalConfig.external = (id: string) => {
return externalPackages.some(
(name) => id === name || id.startsWith(`${name}/`)
);
};
}
if (!global.NX_GRAPH_CREATION) {
finalConfig.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(
projectRoot,
tsConfig,
options,
dependencies
),
},
}),
typeDefinitions({
projectRoot,
}),
postcss({
inject: true,
extract: options.extractCss,
autoModules: true,
plugins: [autoprefixer],
use: {
less: {
javascriptEnabled: options.javascriptEnabled,
},
},
}),
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(
workspaceRoot,
projectNode.data.sourceRoot ?? projectNode.data.root
),
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/,
}),
commonjs(),
analyze(),
generatePackageJson(options, packageJson),
];
if (Array.isArray(rollupConfig.plugins)) {
finalConfig.plugins.push(...rollupConfig.plugins);
}
if (options.deleteOutputPath) {
finalConfig.plugins.push(
deleteOutput({
dirs: Array.isArray(finalConfig.output)
? finalConfig.output.map((o) => o.dir)
: [finalConfig.output.dir],
})
);
}
}
return finalConfig;
}
function createInput(
options: RollupWithNxPluginOptions
): Record<string, string> {
const mainEntryFileName = options.outputFileName || options.main;
const input: Record<string, string> = {};
input[parse(mainEntryFileName).name] = join(workspaceRoot, options.main);
options.additionalEntryPoints?.forEach((entry) => {
input[parse(entry).name] = join(workspaceRoot, entry);
});
return input;
}
function createTsCompilerOptions(
projectRoot: string,
config: ts.ParsedCommandLine,
options: RollupWithNxPluginOptions,
dependencies?: DependentBuildableProjectNode[]
) {
const compilerOptionPaths = computeCompilerOptionsPaths(
config,
dependencies ?? []
);
const compilerOptions = {
rootDir: projectRoot,
allowJs: options.allowJs,
declaration: true,
paths: compilerOptionPaths,
};
if (config.options.module === ts.ModuleKind.CommonJS) {
compilerOptions['module'] = 'ESNext';
}
if (options.compiler === 'swc') {
compilerOptions['emitDeclarationOnly'] = true;
}
return compilerOptions;
}
interface RollupCopyAssetOption {
src: string;
dest: string;
}
function convertCopyAssetsToRollupOptions(
outputPath: string,
assets: AssetGlobPattern[]
): RollupCopyAssetOption[] {
return assets
? assets.map((a) => ({
src: join(a.input, a.glob).replace(/\\/g, '/'),
dest: join(workspaceRoot, outputPath, a.output).replace(/\\/g, '/'),
}))
: undefined;
}
function readCompatibleFormats(
config: ts.ParsedCommandLine
): ('cjs' | 'esm')[] {
switch (config.options.module) {
case ts.ModuleKind.CommonJS:
case ts.ModuleKind.UMD:
case ts.ModuleKind.AMD:
return ['cjs'];
default:
return ['esm'];
}
}

View File

@ -0,0 +1,10 @@
import { readNxJson, Tree } from '@nx/devkit';
export function hasPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
return !!nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/rollup/plugin'
: p.plugin === '@nx/rollup/plugin'
);
}

View File

@ -1,5 +1,5 @@
export const nxVersion = require('../../package.json').version;
export const coreJsVersion = '^3.36.1';
export const rollupVersion = '^4.14.0';
export const swcLoaderVersion = '0.1.15';
export const tsLibVersion = '^2.3.0';
export const coreJsVersion = '^3.36.1';

View File

@ -0,0 +1,2 @@
export { withNx } from './src/plugins/with-nx/with-nx';
export type { RollupWithNxPluginOptions } from './src/plugins/with-nx/with-nx-options';