feat(bundling): add esbuild plugin (#12053)

This commit is contained in:
Jack Hsu 2022-09-20 12:05:58 -04:00 committed by GitHub
parent 38e4a8e7d0
commit ed7db7c114
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1744 additions and 156 deletions

View File

@ -0,0 +1,254 @@
{
"githubRoot": "https://github.com/nrwl/nx/blob/master",
"name": "esbuild",
"packageName": "@nrwl/esbuild",
"description": "The Nx Plugin for EsBuild contains executors and generators that support building applications using EsBuild",
"root": "/packages/esbuild",
"source": "/packages/esbuild/src",
"documentation": [],
"generators": [
{
"name": "init",
"factory": "./src/generators/init/init#esbuildInitGenerator",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxWebpackInit",
"cli": "nx",
"title": "Init Webpack Plugin",
"description": "Init Webpack Plugin.",
"type": "object",
"properties": {
"compiler": {
"type": "string",
"enum": ["babel", "swc", "tsc"],
"description": "The compiler to initialize for.",
"default": "babel"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
}
},
"required": [],
"presets": []
},
"description": "Initialize the `@nrwl/esbuild` plugin.",
"aliases": ["ng-add"],
"hidden": true,
"implementation": "/packages/esbuild/src/generators/init/init#esbuildInitGenerator.ts",
"path": "/packages/esbuild/src/generators/init/schema.json"
},
{
"name": "esbuild-project",
"factory": "./src/generators/esbuild-project/esbuild-project#esbuildProjectGenerator",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxEsBuildProject",
"cli": "nx",
"title": "Add EsBuild Configuration to a project",
"description": "Add EsBuild Configuration to a project.",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "argv", "index": 0 },
"x-dropdown": "project",
"x-prompt": "What is the name of the project to set up a esbuild for?"
},
"main": {
"type": "string",
"description": "Path relative to the workspace root for the main entry file. Defaults to '<projectRoot>/src/main.ts'.",
"alias": "entryFile"
},
"tsConfig": {
"type": "string",
"description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '<projectRoot>/tsconfig.app.json'."
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
},
"importPath": {
"type": "string",
"description": "The library name used to import it, like `@myorg/my-awesome-lib`."
}
},
"required": [],
"presets": []
},
"description": "Add esbuild configuration to a project.",
"hidden": true,
"implementation": "/packages/esbuild/src/generators/esbuild-project/esbuild-project#esbuildProjectGenerator.ts",
"aliases": [],
"path": "/packages/esbuild/src/generators/esbuild-project/schema.json"
}
],
"executors": [
{
"name": "esbuild",
"implementation": "/packages/esbuild/src/executors/esbuild/esbuild.impl.ts",
"schema": {
"title": "Web Library EsBuild Target (Experimental)",
"description": "Packages a library for different web usages (`UMD`, `ESM`, `CJS`).",
"cli": "nx",
"type": "object",
"properties": {
"main": {
"type": "string",
"description": "The path to the entry file, relative to project.",
"alias": "entryFile",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts)"
},
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory"
},
"outputFileName": {
"type": "string",
"description": "Name of the main output file. Defaults same basename as 'main' file."
},
"tsConfig": {
"type": "string",
"description": "The path to tsconfig file.",
"x-completion-type": "file",
"x-completion-glob": "tsconfig.*.json"
},
"project": {
"type": "string",
"description": "The path to package.json file."
},
"format": {
"type": "array",
"description": "Set the output format(s).",
"alias": "f",
"items": { "type": "string", "enum": ["esm", "cjs"] },
"default": ["esm"]
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": false
},
"assets": {
"type": "array",
"description": "List of static assets.",
"default": [],
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply `glob`. Defaults to the project root."
},
"output": {
"type": "string",
"description": "Relative path within the output folder."
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{ "type": "string" }
]
}
},
"clean": {
"type": "boolean",
"description": "Remove previous output before build.",
"default": true
},
"external": {
"type": "array",
"description": "Mark one or more module as external. Can use * wildcards, such as '*.png'.",
"items": { "type": "string" }
},
"metafile": {
"type": "boolean",
"description": "Generate a meta.json file in the output folder that includes metadata about the build. This file can be analyzed by other tools.",
"default": false
},
"minify": {
"type": "boolean",
"description": "Minifies outputs.",
"default": false
},
"platform": {
"type": "string",
"description": "Platform target for outputs.",
"enum": ["browser", "node", "neutral"],
"default": "node"
},
"target": {
"type": "string",
"description": "The environment target for outputs.",
"default": "esnext"
},
"skipTypeCheck": {
"type": "boolean",
"description": "Skip type-checking via TypeScript. Skipping type-checking speeds up the build but type errors are not caught.",
"default": false
},
"updateBuildableProjectDepsInPackageJson": {
"type": "boolean",
"description": "Update buildable project dependencies in `package.json`.",
"default": true
},
"buildableProjectDepsInPackageJsonType": {
"type": "string",
"description": "When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`.",
"enum": ["dependencies", "peerDependencies"],
"default": "peerDependencies"
}
},
"required": ["tsConfig", "main", "outputPath"],
"definitions": {
"assetPattern": {
"oneOf": [
{
"type": "object",
"properties": {
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply `glob`. Defaults to the project root."
},
"output": {
"type": "string",
"description": "Relative path within the output folder."
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{ "type": "string" }
]
}
},
"presets": []
},
"description": "Bundle a package using EsBuild.",
"aliases": [],
"hidden": false,
"path": "/packages/esbuild/src/executors/esbuild/schema.json"
}
]
}

View File

@ -126,6 +126,11 @@
"default": "tsc",
"description": "The compiler used by the build and test targets"
},
"bundler": {
"description": "The bundler to use.",
"enum": ["none", "esbuild", "rollup", "webpack"],
"default": "none"
},
"skipTypeCheck": {
"type": "boolean",
"description": "Whether to skip TypeScript type checking for SWC compiler.",

View File

@ -109,6 +109,16 @@
"path": "generated/packages/devkit.json",
"schemas": { "executors": [], "generators": [] }
},
{
"name": "esbuild",
"packageName": "esbuild",
"description": "The Nx Plugin for EsBuild contains executors and generators that support building applications using EsBuild",
"path": "generated/packages/esbuild.json",
"schemas": {
"executors": ["esbuild"],
"generators": ["init", "esbuild-project"]
}
},
{
"name": "eslint-plugin-nx",
"packageName": "eslint-plugin-nx",

View File

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
maxWorkers: 1,
globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' } },
displayName: 'e2e-esbuild',
preset: '../../jest.preset.js',
};

34
e2e/esbuild/project.json Normal file
View File

@ -0,0 +1,34 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "e2e/esbuild",
"projectType": "application",
"targets": {
"e2e": {
"executor": "nx:run-commands",
"options": {
"commands": [
{
"command": "yarn e2e-start-local-registry"
},
{
"command": "yarn e2e-build-package-publish"
},
{
"command": "nx run-e2e-tests e2e-esbuild"
}
],
"parallel": false
}
},
"run-e2e-tests": {
"executor": "@nrwl/jest:jest",
"options": {
"jestConfig": "e2e/esbuild/jest.config.ts",
"passWithNoTests": true,
"runInBand": true
},
"outputs": ["coverage/e2e/esbuild"]
}
},
"implicitDependencies": ["esbuild"]
}

View File

@ -0,0 +1,63 @@
import {
checkFilesExist,
checkFilesDoNotExist,
cleanupProject,
newProject,
readFile,
runCLI,
runCommand,
uniq,
updateFile,
updateJson,
updateProjectConfig,
} from '@nrwl/e2e/utils';
describe('EsBuild Plugin', () => {
beforeEach(() => newProject());
afterEach(() => cleanupProject());
it('should setup and build projects using build', async () => {
const myPkg = uniq('my-pkg');
runCLI(`generate @nrwl/js:lib ${myPkg} --bundler=esbuild`);
updateFile(`libs/${myPkg}/src/index.ts`, `console.log('Hello');\n`);
updateProjectConfig(myPkg, (json) => {
json.targets.build.options.assets = [`libs/${myPkg}/assets/*`];
return json;
});
updateFile(`libs/${myPkg}/assets/a.md`, 'file a');
updateFile(`libs/${myPkg}/assets/b.md`, 'file b');
runCLI(`build ${myPkg}`);
expect(runCommand(`node dist/libs/${myPkg}/main.js`)).toMatch(/Hello/);
checkFilesExist(`dist/libs/${myPkg}/package.json`);
expect(readFile(`dist/libs/${myPkg}/assets/a.md`)).toMatch(/file a/);
expect(readFile(`dist/libs/${myPkg}/assets/b.md`)).toMatch(/file b/);
/* CJS format is not used by default, but passing --format=esm,cjs generates with it.
*/
checkFilesDoNotExist(`dist/libs/${myPkg}/main.cjs`);
runCLI(`build ${myPkg} --format=esm,cjs`);
checkFilesExist(`dist/libs/${myPkg}/main.cjs`);
/* Metafile is not generated by default, but passing --metafile generates it.
*/
checkFilesDoNotExist(`dist/libs/${myPkg}/meta.json`);
runCLI(`build ${myPkg} --metafile`);
checkFilesExist(`dist/libs/${myPkg}/meta.json`);
/* Type errors are turned on by default
*/
updateFile(
`libs/${myPkg}/src/index.ts`,
`
const x: number = 'a'; // type error
console.log('Bye');
`
);
expect(() => runCLI(`build ${myPkg}`)).toThrow();
expect(() => runCLI(`build ${myPkg} --skipTypeCheck`)).not.toThrow();
expect(runCommand(`node dist/libs/${myPkg}/main.js`)).toMatch(/Bye/);
}, 300_000);
});

13
e2e/esbuild/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["node", "jest"]
},
"include": [],
"files": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,20 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"**/*.d.ts",
"jest.config.ts"
]
}

View File

@ -100,8 +100,8 @@ describe('Workspace Tests', () => {
checkFilesExist(`dist/libs/${buildableLib}/README.md`);
const json = readJson(`dist/libs/${buildableLib}/package.json`);
expect(json.main).toEqual('./src/index.js');
expect(json.typings).toEqual('./src/index.d.ts');
expect(json.main).toEqual('./src/index.cjs');
expect(json.types).toEqual('./src/index.d.ts');
});
});

View File

@ -308,6 +308,7 @@ export function newProject({
`@nrwl/angular`,
`@nrwl/eslint-plugin-nx`,
`@nrwl/express`,
`@nrwl/esbuild`,
`@nrwl/jest`,
`@nrwl/js`,
`@nrwl/linter`,

View File

@ -153,6 +153,11 @@ describe('nx-dev: Packages Section', () => {
{ title: '@nrwl/detox:build', path: '/packages/detox/executors/build' },
{ title: '@nrwl/detox:test', path: '/packages/detox/executors/test' },
{ title: '@nrwl/devkit', path: '/packages/devkit' },
{ title: '@nrwl/esbuild', path: '/packages/esbuild' },
{
title: '@nrwl/esbuild:esbuild',
path: '/packages/esbuild/executors/esbuild',
},
{ title: '@nrwl/eslint-plugin-nx', path: '/packages/eslint-plugin-nx' },
{ title: '@nrwl/express', path: '/packages/express' },
{ title: '@nrwl/express:init', path: '/packages/express/generators/init' },

View File

@ -0,0 +1,4 @@
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="100" fill="#000"/>
<path d="M47.5 52.5L95 100l-47.5 47.5m60-95L155 100l-47.5 47.5" fill="none" stroke="#FFF" stroke-width="24"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@ -4,6 +4,7 @@ export const iconsMap: Record<string, string> = {
cypress: '/images/icons/cypress.svg',
detox: '/images/icons/react.svg',
devkit: '/images/icons/nx.svg',
esbuild: '/images/icons/esbuild.svg',
'eslint-plugin-nx': '/images/icons/eslint.svg',
expo: '/images/icons/expo.svg',
express: '/images/icons/express.svg',

View File

@ -0,0 +1,25 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
},
{
"files": ["./package.json", "./generators.json", "./executors.json"],
"parser": "jsonc-eslint-parser",
"rules": {
"@nrwl/nx/nx-plugin-checks": "error"
}
}
]
}

View File

@ -0,0 +1,13 @@
<p style="text-align: center;"><img src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx.png" width="600" alt="Nx - Smart, Fast and Extensible Build System"></p>
{{links}}
<hr>
# Nx: Smart, Fast and Extensible Build System
Nx is a next generation build system with first class monorepo support and powerful integrations.
This package is a [EsBuild plugin for Nx](https://nx.dev/packages/esbuild).
{{content}}

View File

@ -0,0 +1,16 @@
{
"builders": {
"esbuild": {
"implementation": "./src/executors/esbuild/compat",
"schema": "./src/executors/esbuild/schema.json",
"description": "Bundle a package using EsBuild."
}
},
"executors": {
"esbuild": {
"implementation": "./src/executors/esbuild/esbuild.impl",
"schema": "./src/executors/esbuild/schema.json",
"description": "Bundle a package using EsBuild."
}
}
}

View File

@ -0,0 +1,33 @@
{
"name": "Nx esbuild",
"version": "0.1",
"schematics": {
"init": {
"factory": "./src/generators/init/init#esbuildInitSchematic",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the `@nrwl/esbuild` plugin.",
"hidden": true
},
"esbuild-project": {
"factory": "./src/generators/esbuild-project/esbuild-project#esbuildProjectSchematic",
"schema": "./src/generators/esbuild-project/schema.json",
"description": "Add esbuild configuration to a project.",
"hidden": true
}
},
"generators": {
"init": {
"factory": "./src/generators/init/init#esbuildInitGenerator",
"schema": "./src/generators/init/schema.json",
"description": "Initialize the `@nrwl/esbuild` plugin.",
"aliases": ["ng-add"],
"hidden": true
},
"esbuild-project": {
"factory": "./src/generators/esbuild-project/esbuild-project#esbuildProjectGenerator",
"schema": "./src/generators/esbuild-project/schema.json",
"description": "Add esbuild configuration to a project.",
"hidden": true
}
}
}

View File

@ -0,0 +1,2 @@
export * from './src/executors/esbuild/schema';
export * from './src/executors/esbuild/esbuild.impl';

View File

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' } },
displayName: 'esbuild',
testEnvironment: 'node',
preset: '../../jest.preset.js',
};

View File

@ -0,0 +1,39 @@
{
"name": "@nrwl/esbuild",
"version": "0.0.1",
"description": "The Nx Plugin for EsBuild contains executors and generators that support building applications using EsBuild",
"repository": {
"type": "git",
"url": "https://github.com/nrwl/nx.git",
"directory": "packages/esbuild"
},
"keywords": [
"Monorepo",
"EsBuild",
"Web",
"CLI"
],
"main": "./index.js",
"typings": "./index.d.ts",
"author": "Victor Savkin",
"license": "MIT",
"bugs": {
"url": "https://github.com/nrwl/nx/issues"
},
"homepage": "https://nx.dev",
"schematics": "./generators.json",
"builders": "./executors.json",
"ng-update": {
"requirements": {},
"migrations": "./migrations.json"
},
"dependencies": {
"@nrwl/devkit": "file:../devkit",
"@nrwl/js": "file:../js",
"@nrwl/workspace": "file:../workspace",
"dotenv": "~10.0.0",
"esbuild": "^0.15.7",
"fs-extra": "^10.1.0",
"tslib": "^2.3.0"
}
}

View File

@ -0,0 +1,87 @@
{
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/esbuild",
"projectType": "library",
"targets": {
"test": {
"executor": "@nrwl/jest:jest",
"options": {
"jestConfig": "packages/esbuild/jest.config.ts",
"passWithNoTests": true
},
"outputs": ["coverage/packages/esbuild"]
},
"build-base": {
"executor": "@nrwl/js:tsc",
"options": {
"outputPath": "build/packages/esbuild",
"tsConfig": "packages/esbuild/tsconfig.lib.json",
"main": "packages/esbuild/index.ts",
"updateBuildableProjectDepsInPackageJson": false,
"assets": [
{
"input": "packages/esbuild",
"glob": "**/files/**",
"output": "/"
},
{
"input": "packages/esbuild",
"glob": "**/files/**/.gitkeep",
"output": "/"
},
{
"input": "packages/esbuild",
"glob": "**/*.json",
"ignore": ["**/tsconfig*.json", "project.json", ".eslintrc.json"],
"output": "/"
},
{
"input": "packages/esbuild",
"glob": "**/*.js",
"ignore": ["**/jest.config.js"],
"output": "/"
},
{
"input": "packages/esbuild",
"glob": "**/*.d.ts",
"output": "/"
},
{
"input": "",
"glob": "LICENSE",
"output": "/"
}
]
},
"outputs": ["{options.outputPath}"]
},
"build": {
"executor": "nx:run-commands",
"outputs": ["build/packages/esbuild"],
"options": {
"command": "node ./scripts/copy-readme.js esbuild"
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"options": {
"lintFilePatterns": [
"packages/esbuild/**/*.ts",
"packages/esbuild/**/*.spec.ts",
"packages/esbuild/**/*_spec.ts",
"packages/esbuild/**/*.spec.tsx",
"packages/esbuild/**/*.spec.js",
"packages/esbuild/**/*.spec.jsx",
"packages/esbuild/**/*.d.ts",
"packages/esbuild/**/executors/**/schema.json",
"packages/esbuild/**/generators/**/schema.json",
"packages/esbuild/generators.json",
"packages/esbuild/executors.json",
"packages/esbuild/package.json",
"packages/esbuild/migrations.json"
]
},
"outputs": ["{options.outputFile}"]
}
}
}

View File

@ -0,0 +1,5 @@
import { convertNxExecutor } from '@nrwl/devkit';
import esbuildExecutor from './esbuild.impl';
export default convertNxExecutor(esbuildExecutor);

View File

@ -0,0 +1,192 @@
import 'dotenv/config';
import type { ExecutorContext } from '@nrwl/devkit';
import { cacheDir, joinPathFragments, logger } from '@nrwl/devkit';
import { parse } from 'path';
import {
copyAssets,
copyPackageJson,
printDiagnostics,
runTypeCheck,
TypeCheckOptions,
} from '@nrwl/js';
import * as esbuild from 'esbuild';
import { normalizeOptions } from './lib/normalize';
import { EsBuildExecutorOptions } from './schema';
import { removeSync, writeJsonSync } from 'fs-extra';
import { getClientEnvironment } from '../../utils/environment-variables';
import { createAsyncIterable } from '@nrwl/js/src/utils/create-async-iterable/create-async-iteratable';
export async function* esbuildExecutor(
_options: EsBuildExecutorOptions,
context: ExecutorContext
) {
const options = normalizeOptions(_options);
if (options.clean) removeSync(options.outputPath);
const assetsResult = await copyAssets(options, context);
const packageJsonResult = await copyPackageJson(
{
...options,
skipTypings: options.skipTypeCheck,
},
context
);
const esbuildOptions: esbuild.BuildOptions = {
entryPoints: [options.main],
bundle: true,
define: getClientEnvironment(),
external: options.external,
minify: options.minify,
platform: options.platform,
target: options.target,
metafile: options.metafile,
};
if (options.watch) {
return yield* createAsyncIterable<{ success: boolean; outfile: string }>(
async ({ next, done }) => {
const results = await Promise.all(
options.format.map((format, idx) => {
const outfile = getOutfile(format, options, context);
return esbuild.build({
...esbuildOptions,
metafile: true, // Always include metafile so we can see what files have changed.
watch:
// Only emit info on one of the watch processes.
idx === 0
? {
onRebuild: (
error: esbuild.BuildFailure,
result: esbuild.BuildResult
) => {
if (error) {
logger.info(`[watch] build failed`);
} else if (result?.metafile) {
logger.info(
`[watch] build succeeded (change: "${
Object.keys(result.metafile?.inputs)[0]
}")`
);
} else {
logger.info(`[watch] build succeeded`);
}
next({ success: !!error, outfile });
},
}
: true,
format,
outfile,
});
})
);
logger.info(`[watch] build finished, watching for changes...`);
const processOnExit = () => {
assetsResult?.stop();
packageJsonResult?.stop();
results.forEach((r) => r?.stop());
done();
process.off('SIGINT', processOnExit);
process.off('SIGTERM', processOnExit);
process.off('exit', processOnExit);
};
process.on('SIGINT', processOnExit);
process.on('SIGTERM', processOnExit);
process.on('exit', processOnExit);
next({
success: results.every((r) => r.errors?.length === 0),
outfile: getOutfile(options.format[0], options, context),
});
}
);
} else {
const buildResults = await Promise.all(
options.format.map((format) =>
esbuild.build({
...esbuildOptions,
format,
outfile: getOutfile(format, options, context),
})
)
);
const buildSuccess = buildResults.every((r) => r.errors?.length === 0);
if (options.skipTypeCheck) {
return { success: buildSuccess };
}
const { errors, warnings } = await runTypeCheck(
getTypeCheckOptions(options, context)
);
const hasErrors = errors.length > 0;
const hasWarnings = warnings.length > 0;
if (hasErrors || hasWarnings) {
await printDiagnostics(errors, warnings);
}
if (options.metafile) {
buildResults.forEach((r, idx) => {
const filename =
options.format.length === 1
? 'meta.json'
: `meta.${options.format[idx]}.json`;
writeJsonSync(
joinPathFragments(options.outputPath, filename),
r.metafile
);
});
}
return { success: buildSuccess && !hasErrors };
}
}
function getTypeCheckOptions(
options: EsBuildExecutorOptions,
context: ExecutorContext
) {
const root = context.root;
const projectRoot = context.workspace.projects[context.projectName].root;
const { watch, tsConfig, outputPath } = options;
const typeCheckOptions: TypeCheckOptions = {
mode: 'emitDeclarationOnly',
tsConfigPath: tsConfig,
outDir: outputPath,
workspaceRoot: root,
rootDir: projectRoot,
};
if (watch) {
typeCheckOptions.incremental = true;
typeCheckOptions.cacheDir = cacheDir;
}
return typeCheckOptions;
}
function getOutfile(
format: 'cjs' | 'esm',
options: EsBuildExecutorOptions,
context: ExecutorContext
) {
const candidate = joinPathFragments(
context.target.options.outputPath,
options.outputFileName
);
if (format === 'esm') {
return candidate;
} else {
const { dir, name } = parse(candidate);
return `${dir}/${name}.cjs`;
}
}
export default esbuildExecutor;

View File

@ -0,0 +1,9 @@
import { EsBuildExecutorOptions } from '../schema';
export function normalizeOptions(
options: EsBuildExecutorOptions
): EsBuildExecutorOptions {
return {
...options,
outputFileName: options.outputFileName ?? 'main.js',
};
}

View File

@ -0,0 +1,22 @@
import { AssetGlob } from '@nrwl/workspace/src/utilities/assets';
type Compiler = 'babel' | 'swc';
export interface EsBuildExecutorOptions {
outputPath: string;
tsConfig: string;
project: string;
main: string;
outputFileName?: string;
assets: AssetGlob[];
watch?: boolean;
clean?: boolean;
external?: string[];
format?: Array<'esm' | 'cjs'>;
metafile?: boolean;
minify?: boolean;
platform?: 'node' | 'browser' | 'neutral';
target?: string;
skipTypeCheck?: boolean;
updateBuildableProjectDepsInPackageJson?: boolean;
buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies';
}

View File

@ -0,0 +1,144 @@
{
"title": "Web Library EsBuild Target (Experimental)",
"description": "Packages a library for different web usages (`UMD`, `ESM`, `CJS`).",
"cli": "nx",
"type": "object",
"properties": {
"main": {
"type": "string",
"description": "The path to the entry file, relative to project.",
"alias": "entryFile",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts)"
},
"outputPath": {
"type": "string",
"description": "The output path of the generated files.",
"x-completion-type": "directory"
},
"outputFileName": {
"type": "string",
"description": "Name of the main output file. Defaults same basename as 'main' file."
},
"tsConfig": {
"type": "string",
"description": "The path to tsconfig file.",
"x-completion-type": "file",
"x-completion-glob": "tsconfig.*.json"
},
"project": {
"type": "string",
"description": "The path to package.json file."
},
"format": {
"type": "array",
"description": "List of module formats to output. Defaults to matching format from tsconfig (e.g. CJS for CommonJS, and ESM otherwise).",
"alias": "f",
"items": {
"type": "string",
"enum": ["esm", "umd", "cjs"]
}
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": false
},
"assets": {
"type": "array",
"description": "List of static assets.",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"clean": {
"type": "boolean",
"description": "Remove previous output before build.",
"default": true
},
"external": {
"type": "array",
"description": "Mark one or more module as external. Can use * wildcards, such as '*.png'.",
"items": {
"type": "string"
}
},
"format": {
"type": "array",
"description": "Set the output format(s).",
"alias": "f",
"items": {
"type": "string",
"enum": ["esm", "cjs"]
},
"default": ["esm"]
},
"metafile": {
"type": "boolean",
"description": "Generate a meta.json file in the output folder that includes metadata about the build. This file can be analyzed by other tools.",
"default": false
},
"minify": {
"type": "boolean",
"description": "Minifies outputs.",
"default": false
},
"platform": {
"type": "string",
"description": "Platform target for outputs.",
"enum": ["browser", "node", "neutral"],
"default": "node"
},
"target": {
"type": "string",
"description": "The environment target for outputs.",
"default": "esnext"
},
"skipTypeCheck": {
"type": "boolean",
"description": "Skip type-checking via TypeScript. Skipping type-checking speeds up the build but type errors are not caught.",
"default": false
},
"updateBuildableProjectDepsInPackageJson": {
"type": "boolean",
"description": "Update buildable project dependencies in `package.json`.",
"default": true
},
"buildableProjectDepsInPackageJsonType": {
"type": "string",
"description": "When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`.",
"enum": ["dependencies", "peerDependencies"],
"default": "peerDependencies"
}
},
"required": ["tsConfig", "main", "outputPath"],
"definitions": {
"assetPattern": {
"oneOf": [
{
"type": "object",
"properties": {
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply `glob`. Defaults to the project root."
},
"output": {
"type": "string",
"description": "Relative path within the output folder."
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{
"type": "string"
}
]
}
}
}

View File

@ -0,0 +1,101 @@
import type { Tree } from '@nrwl/devkit';
import {
convertNxGenerator,
formatFiles,
getImportPath,
getWorkspaceLayout,
joinPathFragments,
readProjectConfiguration,
updateProjectConfiguration,
writeJson,
} from '@nrwl/devkit';
import { esbuildInitGenerator } from '../init/init';
import { EsBuildExecutorOptions } from '../../executors/esbuild/schema';
import { EsBuildProjectSchema } from './schema';
export async function esbuildProjectGenerator(
tree: Tree,
options: EsBuildProjectSchema
) {
const task = await esbuildInitGenerator(tree, options);
checkForTargetConflicts(tree, options);
addBuildTarget(tree, options);
await formatFiles(tree);
return task;
}
function checkForTargetConflicts(tree: Tree, options: EsBuildProjectSchema) {
const project = readProjectConfiguration(tree, options.project);
if (project.targets.build) {
throw new Error(`Project "${project.name}" already has a build target.`);
}
if (options.devServer && project.targets.serve) {
throw new Error(`Project "${project.name}" already has a serve target.`);
}
}
function addBuildTarget(tree: Tree, options: EsBuildProjectSchema) {
const project = readProjectConfiguration(tree, options.project);
const packageJsonPath = joinPathFragments(project.root, 'package.json');
if (!tree.exists(packageJsonPath)) {
const { npmScope } = getWorkspaceLayout(tree);
const importPath =
options.importPath || getImportPath(npmScope, options.project);
writeJson(tree, packageJsonPath, {
name: importPath,
version: '0.0.1',
});
}
const tsConfig =
options.tsConfig ?? joinPathFragments(project.root, 'tsconfig.lib.json');
const buildOptions: EsBuildExecutorOptions = {
main: options.main ?? joinPathFragments(project.root, 'src/main.ts'),
outputPath: joinPathFragments('dist', project.root),
outputFileName: 'main.js',
tsConfig,
project: `${project.root}/package.json`,
assets: [],
};
if (tree.exists(joinPathFragments(project.root, 'README.md'))) {
buildOptions.assets = [
{
glob: `${project.root}/README.md`,
input: '.',
output: '.',
},
];
}
updateProjectConfiguration(tree, options.project, {
...project,
targets: {
...project.targets,
build: {
executor: '@nrwl/esbuild:esbuild',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: buildOptions,
configurations: {
production: {
optimization: true,
sourceMap: false,
namedChunks: false,
extractLicenses: true,
vendorChunk: false,
},
},
},
},
});
}
export default esbuildProjectGenerator;
export const esbuildProjectSchematic = convertNxGenerator(
esbuildProjectGenerator
);

View File

@ -0,0 +1,10 @@
export interface EsBuildProjectSchema {
project: string;
main?: string;
tsConfig?: string;
devServer?: boolean;
skipFormat?: boolean;
skipPackageJson?: boolean;
importPath?: string;
esbuildConfig?: string;
}

View File

@ -0,0 +1,44 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxEsBuildProject",
"cli": "nx",
"title": "Add EsBuild Configuration to a project",
"description": "Add EsBuild Configuration to a project.",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"$default": {
"$source": "argv",
"index": 0
},
"x-dropdown": "project",
"x-prompt": "What is the name of the project to set up a esbuild for?"
},
"main": {
"type": "string",
"description": "Path relative to the workspace root for the main entry file. Defaults to '<projectRoot>/src/main.ts'.",
"alias": "entryFile"
},
"tsConfig": {
"type": "string",
"description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '<projectRoot>/tsconfig.app.json'."
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
},
"skipPackageJson": {
"type": "boolean",
"default": false,
"description": "Do not add dependencies to `package.json`."
},
"importPath": {
"type": "string",
"description": "The library name used to import it, like `@myorg/my-awesome-lib`."
}
},
"required": []
}

View File

@ -0,0 +1,16 @@
import { Tree } from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { esbuildInitGenerator } from './init';
describe('esbuildInitGenerator', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace();
});
it('should run successfully', async () => {
await expect(esbuildInitGenerator(tree, {})).resolves.not.toThrow();
});
});

View File

@ -0,0 +1,15 @@
import { convertNxGenerator, formatFiles, Tree } from '@nrwl/devkit';
import { Schema } from './schema';
export async function esbuildInitGenerator(tree: Tree, schema: Schema) {
/*
* Empty for now, might need to add setup files later.
*/
if (!schema.skipFormat) {
await formatFiles(tree);
}
}
export default esbuildInitGenerator;
export const esbuildInitSchematic = convertNxGenerator(esbuildInitGenerator);

View File

@ -0,0 +1,4 @@
export interface Schema {
compiler?: 'babel' | 'swc' | 'tsc';
skipFormat?: boolean;
}

View File

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxWebpackInit",
"cli": "nx",
"title": "Init Webpack Plugin",
"description": "Init Webpack Plugin.",
"type": "object",
"properties": {
"compiler": {
"type": "string",
"enum": ["babel", "swc", "tsc"],
"description": "The compiler to initialize for.",
"default": "babel"
},
"skipFormat": {
"description": "Skip formatting files.",
"type": "boolean",
"default": false
}
},
"required": []
}

View File

@ -0,0 +1,10 @@
export function getClientEnvironment(): Record<string, string> {
const NX_APP = /^NX_/i;
return Object.keys(process.env)
.filter((key) => NX_APP.test(key) || key === 'NODE_ENV')
.reduce((env, key) => {
env[`process.env.${key}`] = JSON.stringify(process.env[key]);
return env;
}, {});
}

View File

@ -0,0 +1,14 @@
import * as path from 'path';
import { removeSync } from 'fs-extra';
/**
* Delete an output directory, but error out if it's the root of the project.
*/
export function deleteOutputDir(root: string, outputPath: string) {
const resolvedOutputPath = path.resolve(root, outputPath);
if (resolvedOutputPath === root) {
throw new Error('Output path MUST not be project root directory!');
}
removeSync(resolvedOutputPath);
}

View File

@ -0,0 +1,5 @@
export const nxVersion = require('../../package.json').version;
export const swcLoaderVersion = '0.1.15';
export const swcHelpersVersion = '~0.3.3';
export const tsLibVersion = '^2.3.0';

View File

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": []
},
"include": ["**/*.ts"],
"exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts"]
}

View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
}

View File

@ -1,9 +1,8 @@
import { ExecutorContext, ProjectGraphProjectNode } from '@nrwl/devkit';
import { ExecutorContext } from '@nrwl/devkit';
import {
assetGlobsToFiles,
FileInputOutput,
} from '@nrwl/workspace/src/utilities/assets';
import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { join, relative, resolve } from 'path';
import { checkDependencies } from '../../utils/check-dependencies';
@ -11,15 +10,14 @@ import {
getHelperDependency,
HelperDependency,
} from '../../utils/compiler-helper-dependency';
import { CopyAssetsHandler } from '../../utils/copy-assets-handler';
import {
NormalizedSwcExecutorOptions,
SwcExecutorOptions,
} from '../../utils/schema';
import { compileSwc, compileSwcWatch } from '../../utils/swc/compile-swc';
import { getSwcrcPath } from '../../utils/swc/get-swcrc-path';
import { updatePackageJson } from '../../utils/update-package-json';
import { watchForSingleFileChanges } from '../../utils/watch-for-single-file-changes';
import { copyAssets } from '../../utils/assets';
import { copyPackageJson } from '../../utils/package-json';
export function normalizeOptions(
options: SwcExecutorOptions,
@ -75,32 +73,13 @@ export function normalizeOptions(
} as NormalizedSwcExecutorOptions;
}
function processAssetsAndPackageJsonOnce(
assetHandler: CopyAssetsHandler,
options: NormalizedSwcExecutorOptions,
context: ExecutorContext,
target: ProjectGraphProjectNode<any>,
dependencies: DependentBuildableProjectNode[]
) {
return async () => {
await assetHandler.processAllAssetsOnce();
updatePackageJson(
options,
context,
target,
dependencies,
!options.skipTypeCheck
);
};
}
export async function* swcExecutor(
_options: SwcExecutorOptions,
context: ExecutorContext
) {
const { sourceRoot, root } = context.workspace.projects[context.projectName];
const options = normalizeOptions(_options, context.root, sourceRoot, root);
const { tmpTsConfig, projectRoot, target, dependencies } = checkDependencies(
const { tmpTsConfig, dependencies } = checkDependencies(
context,
options.tsConfig
);
@ -120,58 +99,38 @@ export async function* swcExecutor(
dependencies.push(swcHelperDependency);
}
const assetHandler = new CopyAssetsHandler({
projectDir: projectRoot,
rootDir: context.root,
outputDir: options.outputPath,
assets: options.assets,
});
if (options.watch) {
const disposeWatchAssetChanges =
await assetHandler.watchAndProcessOnAssetChange();
const disposePackageJsonChanges = await watchForSingleFileChanges(
join(context.root, projectRoot),
'package.json',
() =>
updatePackageJson(
options,
context,
target,
dependencies,
!options.skipTypeCheck
)
);
const handleTermination = async () => {
await disposeWatchAssetChanges();
await disposePackageJsonChanges();
};
process.on('SIGINT', () => handleTermination());
process.on('SIGTERM', () => handleTermination());
let disposeFn: () => void;
process.on('SIGINT', () => disposeFn());
process.on('SIGTERM', () => disposeFn());
return yield* compileSwcWatch(
context,
options,
processAssetsAndPackageJsonOnce(
assetHandler,
options,
context,
target,
dependencies
)
);
return yield* compileSwcWatch(context, options, async () => {
const assetResult = await copyAssets(options, context);
const packageJsonResult = await copyPackageJson(
{
...options,
skipTypings: !options.skipTypeCheck,
},
context
);
disposeFn = () => {
assetResult?.stop();
packageJsonResult?.stop();
};
});
} else {
return yield compileSwc(
context,
options,
processAssetsAndPackageJsonOnce(
assetHandler,
options,
context,
target,
dependencies
)
);
return yield compileSwc(context, options, async () => {
await copyAssets(options, context);
await copyPackageJson(
{
...options,
generateExportsField: true,
skipTypings: !options.skipTypeCheck,
extraDependencies: swcHelperDependency ? [swcHelperDependency] : [],
},
context
);
});
}
}

View File

@ -16,11 +16,11 @@ import {
getHelperDependency,
HelperDependency,
} from '../../utils/compiler-helper-dependency';
import { CopyAssetsHandler } from '../../utils/copy-assets-handler';
import { CopyAssetsHandler } from '../../utils/assets/copy-assets-handler';
import { ExecutorOptions, NormalizedExecutorOptions } from '../../utils/schema';
import { compileTypeScriptFiles } from '../../utils/typescript/compile-typescript-files';
import { loadTsTransformers } from '../../utils/typescript/load-ts-transformers';
import { updatePackageJson } from '../../utils/update-package-json';
import { updatePackageJson } from '../../utils/package-json/update-package-json';
import { watchForSingleFileChanges } from '../../utils/watch-for-single-file-changes';
export function normalizeOptions(

View File

@ -1,4 +1,5 @@
import {
addDependenciesToPackageJson,
addProjectConfiguration,
convertNxGenerator,
formatFiles,
@ -9,10 +10,10 @@ import {
names,
offsetFromRoot,
ProjectConfiguration,
readJson,
toJS,
Tree,
updateJson,
readJson,
writeJson,
} from '@nrwl/devkit';
import { getImportPath } from 'nx/src/utils/path';
@ -28,6 +29,7 @@ import { addMinimalPublishScript } from '../../utils/minimal-publish-script';
import { LibraryGeneratorSchema } from '../../utils/schema';
import { addSwcConfig } from '../../utils/swc/add-swc-config';
import { addSwcDependencies } from '../../utils/swc/add-swc-dependencies';
import { nxVersion } from '../../utils/versions';
export async function libraryGenerator(
tree: Tree,
@ -43,18 +45,19 @@ export async function projectGenerator(
destinationDir: string,
filesDir: string
) {
const tasks: GeneratorCallback[] = [];
const options = normalizeOptions(tree, schema, destinationDir);
createFiles(tree, options, `${filesDir}/lib`);
addProject(tree, options, destinationDir);
// tasks.push(addProjectDependencies(tree, options));
if (!schema.skipTsConfig) {
updateRootTsConfig(tree, options);
}
const tasks: GeneratorCallback[] = [];
if (options.linter !== 'none') {
const lintCallback = await addLint(tree, options);
tasks.push(lintCallback);
@ -99,7 +102,7 @@ function addProject(
if (options.buildable && options.config !== 'npm-scripts') {
const outputPath = `dist/${destinationDir}/${options.projectDirectory}`;
projectConfiguration.targets.build = {
executor: `@nrwl/js:${options.compiler}`,
executor: getBuildExecutor(options),
outputs: ['{options.outputPath}'],
options: {
outputPath,
@ -408,5 +411,50 @@ function updateRootTsConfig(host: Tree, options: NormalizedSchema) {
});
}
function addProjectDependencies(
tree: Tree,
options: NormalizedSchema
): GeneratorCallback {
if (options.bundler == 'esbuild') {
return addDependenciesToPackageJson(
tree,
{},
{ '@nrwl/esbuild': nxVersion }
);
}
if (options.bundler == 'rollup') {
return addDependenciesToPackageJson(
tree,
{},
{ '@nrwl/rollup': nxVersion }
);
}
if (options.bundler == 'webpack') {
return addDependenciesToPackageJson(
tree,
{},
{ '@nrwl/webpack': nxVersion }
);
}
// noop
return () => {};
}
function getBuildExecutor(options: NormalizedSchema) {
switch (options.bundler) {
case 'esbuild':
return `@nrwl/esbuild:esbuild`;
case 'rollup':
return `@nrwl/rollup:rollup`;
case 'webpack':
return `@nrwl/webpack:webpack`;
default:
return `@nrwl/js:${options.compiler}`;
}
}
export default libraryGenerator;
export const librarySchematic = convertNxGenerator(libraryGenerator);

View File

@ -109,6 +109,11 @@
"default": "tsc",
"description": "The compiler used by the build and test targets"
},
"bundler": {
"description": "The bundler to use.",
"enum": ["none", "esbuild", "rollup", "webpack"],
"default": "none"
},
"skipTypeCheck": {
"type": "boolean",
"description": "Whether to skip TypeScript type checking for SWC compiler.",

View File

@ -1,4 +1,7 @@
export * from './utils/typescript/load-ts-transformers';
export * from './utils/typescript/print-diagnostics';
export * from './utils/typescript/run-type-check';
export * from './utils/package-json';
export * from './utils/assets';
export * from './utils/package-json/update-package-json';
export { libraryGenerator } from './generators/library/library';

View File

@ -0,0 +1,47 @@
import { AssetGlob } from '@nrwl/workspace/src/utilities/assets';
import { CopyAssetsHandler, FileEvent } from './copy-assets-handler';
import { ExecutorContext } from '@nrwl/devkit';
export interface CopyAssetsOptions {
outputPath: string;
assets: (string | AssetGlob)[];
watch?: boolean | WatchMode;
}
export interface CopyAssetsResult {
success?: boolean;
// Only when "watch: true"
stop?: () => void;
}
export interface WatchMode {
onCopy?: (events: FileEvent[]) => void;
}
export async function copyAssets(
options: CopyAssetsOptions,
context: ExecutorContext
): Promise<CopyAssetsResult> {
const assetHandler = new CopyAssetsHandler({
projectDir: context.workspace.projects[context.projectName].root,
rootDir: context.root,
outputDir: options.outputPath,
assets: options.assets,
callback:
typeof options?.watch === 'object' ? options.watch.onCopy : undefined,
});
if (options.watch) {
const dispose = await assetHandler.watchAndProcessOnAssetChange();
return {
success: true,
stop: dispose,
};
} else {
try {
await assetHandler.processAllAssetsOnce();
} catch {
return { success: false };
}
return { success: true };
}
}

View File

@ -0,0 +1,52 @@
import { join } from 'path';
import { ExecutorContext } from '@nrwl/devkit';
import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { watchForSingleFileChanges } from '../watch-for-single-file-changes';
import type { UpdatePackageJsonOption } from './update-package-json';
import { updatePackageJson } from './update-package-json';
import { checkDependencies } from '../check-dependencies';
export interface CopyPackageJsonOptions
extends Omit<UpdatePackageJsonOption, 'projectRoot'> {
watch?: boolean;
extraDependencies?: DependentBuildableProjectNode[];
}
export interface CopyPackageJsonResult {
success?: boolean;
// Only when "watch: true"
stop?: () => void;
}
export async function copyPackageJson(
_options: CopyPackageJsonOptions,
context: ExecutorContext
): Promise<CopyPackageJsonResult> {
if (!context.target.options.tsConfig) {
throw new Error(
`Could not find tsConfig option for "${context.targetName}" target of "${context.projectName}" project. Check that your project configuration is correct.`
);
}
const { target, dependencies, projectRoot } = checkDependencies(
context,
context.target.options.tsConfig
);
const options = { ..._options, projectRoot };
if (options.extraDependencies?.length) {
dependencies.push(...options.extraDependencies);
}
if (options.watch) {
const dispose = await watchForSingleFileChanges(
join(context.root, projectRoot),
'package.json',
() => updatePackageJson(options, context, target, dependencies)
);
// Copy it once before changes
updatePackageJson(options, context, target, dependencies);
return { success: true, stop: dispose };
} else {
updatePackageJson(options, context, target, dependencies);
return { success: true };
}
}

View File

@ -0,0 +1,119 @@
import { getUpdatedPackageJsonContent } from './update-package-json';
describe('getUpdatedPackageJsonContent', () => {
it('should update fields for commonjs only (default)', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
}
);
expect(json).toEqual({
name: 'test',
main: './src/index.cjs',
types: './src/index.d.ts',
version: '0.0.1',
});
});
it('should update fields for esm only', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['esm'],
}
);
expect(json).toEqual({
name: 'test',
type: 'module',
module: './src/index.js',
main: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
});
});
it('should update fields for commonjs + esm', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['esm', 'cjs'],
}
);
expect(json).toEqual({
name: 'test',
main: './src/index.cjs',
module: './src/index.js',
types: './src/index.d.ts',
version: '0.0.1',
});
});
it('should support skipping types', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
skipTypings: true,
}
);
expect(json).toEqual({
name: 'test',
main: './src/index.cjs',
version: '0.0.1',
});
});
it('should support generated exports field', () => {
const json = getUpdatedPackageJsonContent(
{
name: 'test',
version: '0.0.1',
},
{
main: 'proj/src/index.ts',
outputPath: 'dist/proj',
projectRoot: 'proj',
format: ['esm', 'cjs'],
generateExportsField: true,
}
);
expect(json).toEqual({
name: 'test',
module: './src/index.js',
main: './src/index.cjs',
types: './src/index.d.ts',
version: '0.0.1',
exports: {
'.': { require: './src/index.cjs', import: './src/index.js' },
},
});
});
});

View File

@ -0,0 +1,124 @@
import {
ExecutorContext,
normalizePath,
ProjectGraphProjectNode,
readJsonFile,
writeJsonFile,
} from '@nrwl/devkit';
import {
DependentBuildableProjectNode,
updateBuildableProjectPackageJsonDependencies,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { basename, dirname, join, parse, relative } from 'path';
import { fileExists } from 'nx/src/utils/fileutils';
import type { PackageJson } from 'nx/src/utils/package-json';
function getMainFileDirRelativeToProjectRoot(
main: string,
projectRoot: string
): string {
const mainFileDir = dirname(main);
const relativeDir = normalizePath(relative(projectRoot, mainFileDir));
return relativeDir === '' ? `./` : `./${relativeDir}/`;
}
export interface UpdatePackageJsonOption {
projectRoot: string;
outputPath: string;
main: string;
format?: string[];
skipTypings?: boolean;
outputFileName?: string;
generateExportsField?: boolean;
updateBuildableProjectDepsInPackageJson?: boolean;
buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies';
}
export function updatePackageJson(
options: UpdatePackageJsonOption,
context: ExecutorContext,
target: ProjectGraphProjectNode,
dependencies: DependentBuildableProjectNode[]
): void {
const pathToPackageJson = join(
context.root,
options.projectRoot,
'package.json'
);
const packageJson = fileExists(pathToPackageJson)
? readJsonFile(pathToPackageJson)
: { name: context.projectName };
writeJsonFile(
`${options.outputPath}/package.json`,
getUpdatedPackageJsonContent(packageJson, options)
);
if (
dependencies.length > 0 &&
options.updateBuildableProjectDepsInPackageJson
) {
updateBuildableProjectPackageJsonDependencies(
context.root,
context.projectName,
context.targetName,
context.configurationName,
target,
dependencies,
options.buildableProjectDepsInPackageJsonType
);
}
}
export function getUpdatedPackageJsonContent(
packageJson: PackageJson,
options: UpdatePackageJsonOption
) {
// Default is CJS unless esm is explicitly passed.
const hasCjsFormat = !options.format || options.format?.includes('cjs');
const hasEsmFormat = options.format?.includes('esm');
const mainFile = basename(options.main).replace(/\.[tj]s$/, '');
const relativeMainFileDir = getMainFileDirRelativeToProjectRoot(
options.main,
options.projectRoot
);
const typingsFile = `${relativeMainFileDir}${mainFile}.d.ts`;
const exports = {
'.': {},
...packageJson.exports,
};
const mainJsFile =
options.outputFileName ?? `${relativeMainFileDir}${mainFile}.js`;
if (hasEsmFormat) {
// Unofficial field for backwards compat.
packageJson.module ??= mainJsFile;
if (!hasCjsFormat) {
packageJson.type = 'module';
packageJson.main ??= mainJsFile;
}
exports['.']['import'] = mainJsFile;
}
if (hasCjsFormat) {
const { dir, name } = parse(mainJsFile);
const cjsMain = `${dir}/${name}.cjs`;
packageJson.main ??= cjsMain;
exports['.']['require'] = cjsMain;
}
if (options.generateExportsField) {
packageJson.exports = exports;
}
if (!options.skipTypings) {
packageJson.types = packageJson.types ?? typingsFile;
}
return packageJson;
}

View File

@ -7,6 +7,7 @@ import type {
import { TransformerEntry } from './typescript/types';
export type Compiler = 'tsc' | 'swc';
export type Bundler = 'none' | 'rollup' | 'esbuild' | 'webpack';
export interface LibraryGeneratorSchema {
name: string;
@ -28,6 +29,7 @@ export interface LibraryGeneratorSchema {
setParserOptionsProject?: boolean;
config?: 'workspace' | 'project' | 'npm-scripts';
compiler?: Compiler;
bundler?: Bundler;
skipTypeCheck?: boolean;
}
@ -48,7 +50,7 @@ export interface ExecutorOptions {
export interface NormalizedExecutorOptions extends ExecutorOptions {
root?: string;
sourceRoot?: string;
projectRoot?: string;
projectRoot: string;
mainOutputPath: string;
files: Array<FileInputOutput>;
}

View File

@ -1,72 +0,0 @@
import {
ExecutorContext,
normalizePath,
ProjectGraphProjectNode,
readJsonFile,
writeJsonFile,
} from '@nrwl/devkit';
import {
DependentBuildableProjectNode,
updateBuildableProjectPackageJsonDependencies,
} from '@nrwl/workspace/src/utilities/buildable-libs-utils';
import { basename, dirname, join, relative } from 'path';
import { NormalizedExecutorOptions } from './schema';
import { fileExists } from 'nx/src/utils/fileutils';
function getMainFileDirRelativeToProjectRoot(
main: string,
projectRoot: string
): string {
const mainFileDir = dirname(main);
const relativeDir = normalizePath(relative(projectRoot, mainFileDir));
return relativeDir === '' ? `./` : `./${relativeDir}/`;
}
export function updatePackageJson(
options: NormalizedExecutorOptions,
context: ExecutorContext,
target: ProjectGraphProjectNode<any>,
dependencies: DependentBuildableProjectNode[],
withTypings = true
): void {
const pathToPackageJson = join(
context.root,
options.projectRoot,
'package.json'
);
const packageJson = fileExists(pathToPackageJson)
? readJsonFile(pathToPackageJson)
: { name: context.projectName };
const mainFile = basename(options.main).replace(/\.[tj]s$/, '');
const relativeMainFileDir = getMainFileDirRelativeToProjectRoot(
options.main,
options.projectRoot
);
const mainJsFile = `${relativeMainFileDir}${mainFile}.js`;
const typingsFile = `${relativeMainFileDir}${mainFile}.d.ts`;
packageJson.main = packageJson.main ?? mainJsFile;
if (withTypings) {
packageJson.typings = packageJson.typings ?? typingsFile;
}
writeJsonFile(`${options.outputPath}/package.json`, packageJson);
if (
dependencies.length > 0 &&
options.updateBuildableProjectDepsInPackageJson
) {
updateBuildableProjectPackageJsonDependencies(
context.root,
context.projectName,
context.targetName,
context.configurationName,
target,
dependencies,
options.buildableProjectDepsInPackageJsonType
);
}
}

View File

@ -40,6 +40,7 @@
"autoprefixer": "^10.4.9",
"babel-plugin-transform-async-to-promises": "^0.8.15",
"chalk": "4.1.0",
"dotenv": "~10.0.0",
"fs-extra": "^10.1.0",
"postcss": "^8.4.14",
"rollup": "^2.56.2",

View File

@ -1,3 +1,4 @@
import 'dotenv/config';
import * as ts from 'typescript';
import * as rollup from 'rollup';
import * as peerDepsExternal from 'rollup-plugin-peer-deps-external';

View File

@ -40,6 +40,7 @@
"chokidar": "^3.5.1",
"copy-webpack-plugin": "^10.2.4",
"css-minimizer-webpack-plugin": "^3.4.1",
"dotenv": "~10.0.0",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "7.2.13",
"fs-extra": "^10.1.0",

View File

@ -1,3 +1,4 @@
import 'dotenv/config';
import { ExecutorContext, logger } from '@nrwl/devkit';
import { eachValueFrom } from '@nrwl/devkit/src/utils/rxjs-for-await';
import type { Configuration, Stats } from 'webpack';

View File

@ -7,7 +7,7 @@ export type FileInputOutput = {
};
export type AssetGlob = FileInputOutput & {
glob: string;
ignore: string[];
ignore?: string[];
dot?: boolean;
};

View File

@ -16,6 +16,7 @@
"e2e-angular-extensions": "e2e/angular-extensions",
"e2e-cypress": "e2e/cypress",
"e2e-detox": "e2e/detox",
"e2e-esbuild": "e2e/esbuild",
"e2e-expo": "e2e/expo",
"e2e-graph-client": "graph/client-e2e",
"e2e-jest": "e2e/jest",
@ -37,6 +38,7 @@
"e2e-web": "e2e/web",
"e2e-webpack": "e2e/webpack",
"e2e-workspace-create": "e2e/workspace-create",
"esbuild": "packages/esbuild",
"eslint-plugin-nx": "packages/eslint-plugin-nx",
"eslint-rules": "tools/eslint-rules",
"expo": "packages/expo",