diff --git a/docs/generated/packages/js/executors/swc.json b/docs/generated/packages/js/executors/swc.json index c2ddecf4221f4..871bf81b1d392 100644 --- a/docs/generated/packages/js/executors/swc.json +++ b/docs/generated/packages/js/executors/swc.json @@ -116,6 +116,12 @@ "items": { "type": "string" }, "description": "List of target names that annotate a build target for a project", "default": ["build"] + }, + "generateLockfile": { + "type": "boolean", + "description": "Generate a lockfile (e.g. yarn.lock) that matches the workspace lockfile to ensure package versions match.", + "default": false, + "x-priority": "internal" } }, "required": ["main", "outputPath", "tsConfig"], diff --git a/docs/generated/packages/js/executors/tsc.json b/docs/generated/packages/js/executors/tsc.json index e6de3ce35f8d2..3cc27ffe2cb96 100644 --- a/docs/generated/packages/js/executors/tsc.json +++ b/docs/generated/packages/js/executors/tsc.json @@ -116,6 +116,12 @@ "items": { "type": "string" }, "description": "List of target names that annotate a build target for a project", "default": ["build"] + }, + "generateLockfile": { + "type": "boolean", + "description": "Generate a lockfile (e.g. yarn.lock) that matches the workspace lockfile to ensure package versions match.", + "default": false, + "x-priority": "internal" } }, "required": ["main", "outputPath", "tsConfig"], diff --git a/e2e/js/src/js-swc.test.ts b/e2e/js/src/js-swc.test.ts index aadc991eff8ab..b6ebb923d78b8 100644 --- a/e2e/js/src/js-swc.test.ts +++ b/e2e/js/src/js-swc.test.ts @@ -3,10 +3,13 @@ import { checkFilesDoNotExist, checkFilesExist, cleanupProject, + detectPackageManager, newProject, + packageManagerLockFile, readJson, runCLI, runCLIAsync, + tmpProjPath, uniq, updateFile, updateJson, @@ -80,6 +83,14 @@ describe('js e2e', () => { expect(output).toContain('1 task it depends on'); expect(output).toContain('Successfully compiled: 2 files with swc'); + runCLI(`build ${parentLib} --generateLockfile=true`); + checkFilesExist( + `dist/libs/${parentLib}/package.json`, + `dist/libs/${parentLib}/${ + packageManagerLockFile[detectPackageManager(tmpProjPath())] + }` + ); + updateJson(`libs/${lib}/.lib.swcrc`, (json) => { json.jsc.externalHelpers = true; return json; diff --git a/e2e/js/src/js-tsc.test.ts b/e2e/js/src/js-tsc.test.ts index 63846ecc55fff..b3e49cfebd478 100644 --- a/e2e/js/src/js-tsc.test.ts +++ b/e2e/js/src/js-tsc.test.ts @@ -3,6 +3,7 @@ import { checkFilesDoNotExist, checkFilesExist, cleanupProject, + detectPackageManager, getPackageManagerCommand, newProject, packageManagerLockFile, @@ -12,6 +13,7 @@ import { runCLIAsync, runCommand, runCommandUntil, + tmpProjPath, uniq, updateFile, updateJson, @@ -49,6 +51,14 @@ describe('js e2e', () => { `dist/libs/${lib}/src/lib/${lib}.d.ts` ); + runCLI(`build ${lib} --generateLockfile=true`); + checkFilesExist( + `dist/libs/${lib}/package.json`, + `dist/libs/${lib}/${ + packageManagerLockFile[detectPackageManager(tmpProjPath())] + }` + ); + updateJson(`libs/${lib}/project.json`, (json) => { json.targets.build.options.assets.push({ input: `libs/${lib}/docs`, @@ -187,9 +197,10 @@ describe('package.json updates', () => { runCLI(`build ${lib}`); // Check that only 'react' exists, don't care about version - expect(readJson(`dist/libs/${lib}/package.json`).dependencies).toEqual({}); - expect(readJson(`dist/libs/${lib}/package.json`).peerDependencies).toEqual({ + expect(readJson(`dist/libs/${lib}/package.json`).dependencies).toEqual({ react: expect.any(String), + }); + expect(readJson(`dist/libs/${lib}/package.json`).peerDependencies).toEqual({ tslib: expect.any(String), }); checkFilesDoNotExist(`dist/libs/${lib}/${packageManagerLockFile['npm']}`); diff --git a/packages/js/src/executors/swc/schema.json b/packages/js/src/executors/swc/schema.json index 01e5f3bc87243..77856847d9ee9 100644 --- a/packages/js/src/executors/swc/schema.json +++ b/packages/js/src/executors/swc/schema.json @@ -97,6 +97,12 @@ }, "description": "List of target names that annotate a build target for a project", "default": ["build"] + }, + "generateLockfile": { + "type": "boolean", + "description": "Generate a lockfile (e.g. yarn.lock) that matches the workspace lockfile to ensure package versions match.", + "default": false, + "x-priority": "internal" } }, "required": ["main", "outputPath", "tsConfig"], diff --git a/packages/js/src/executors/tsc/schema.json b/packages/js/src/executors/tsc/schema.json index b73a5ba261810..97b03dfd64750 100644 --- a/packages/js/src/executors/tsc/schema.json +++ b/packages/js/src/executors/tsc/schema.json @@ -86,6 +86,12 @@ }, "description": "List of target names that annotate a build target for a project", "default": ["build"] + }, + "generateLockfile": { + "type": "boolean", + "description": "Generate a lockfile (e.g. yarn.lock) that matches the workspace lockfile to ensure package versions match.", + "default": false, + "x-priority": "internal" } }, "required": ["main", "outputPath", "tsConfig"], diff --git a/packages/js/src/utils/package-json/update-package-json.spec.ts b/packages/js/src/utils/package-json/update-package-json.spec.ts index 75ca39593c420..22eb3b7dbe3c1 100644 --- a/packages/js/src/utils/package-json/update-package-json.spec.ts +++ b/packages/js/src/utils/package-json/update-package-json.spec.ts @@ -1,4 +1,16 @@ -import { getUpdatedPackageJsonContent } from './update-package-json'; +import { + getUpdatedPackageJsonContent, + updatePackageJson, + UpdatePackageJsonOption, +} from './update-package-json'; +import { vol } from 'memfs'; +import { ExecutorContext, ProjectGraph } from '@nrwl/devkit'; +import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils'; + +jest.mock('nx/src/utils/workspace-root', () => ({ + workspaceRoot: '/root', +})); +jest.mock('fs', () => require('memfs').fs); describe('getUpdatedPackageJsonContent', () => { it('should update fields for commonjs only (default)', () => { @@ -259,3 +271,157 @@ describe('getUpdatedPackageJsonContent', () => { }); }); }); + +describe('updatePackageJson', () => { + const originalPackageJson = { + name: '@org/lib1', + version: '0.0.3', + dependencies: { lib2: '^0.0.1' }, + devDependencies: { jest: '27' }, + }; + const rootPackageJson = { + name: '@org/root', + version: '1.2.3', + dependencies: { external1: '~1.0.0', external2: '^4.0.0' }, + devDependencies: { jest: '27' }, + }; + const projectGraph: ProjectGraph = { + nodes: { + '@org/lib1': { + type: 'lib', + name: '@org/lib1', + data: { + root: 'libs/lib1', + targets: { + build: { + outputs: ['{workspaceRoot}/dist/libs/lib1'], + }, + }, + files: [], + }, + }, + }, + externalNodes: { + 'npm:external1': { + type: 'npm', + name: 'npm:external1', + data: { + packageName: 'external1', + version: '1.0.0', + }, + }, + 'npm:external2': { + type: 'npm', + name: 'npm:external2', + data: { + packageName: 'external2', + version: '4.5.6', + }, + }, + 'npm:jest': { + type: 'npm', + name: 'npm:jest', + data: { + packageName: 'jest', + version: '21.1.0', + }, + }, + }, + dependencies: { + '@org/lib1': [ + { source: '@org/lib1', target: 'npm:external1', type: 'static' }, + { source: '@org/lib1', target: 'npm:external2', type: 'static' }, + ], + }, + }; + const context: ExecutorContext = { + root: '/root', + projectName: '@org/lib1', + isVerbose: false, + cwd: '', + targetName: 'build', + projectGraph, + }; + + it('should generate new package if missing', () => { + const fsJson = {}; + vol.fromJSON(fsJson, '/root'); + const options: UpdatePackageJsonOption = { + outputPath: 'dist/libs/lib1', + projectRoot: 'libs/lib1', + main: 'libs/lib1/main.ts', + }; + const dependencies: DependentBuildableProjectNode[] = []; + updatePackageJson(options, context, undefined, dependencies); + + expect(vol.existsSync('dist/libs/lib1/package.json')).toEqual(true); + const distPackageJson = JSON.parse( + vol.readFileSync('dist/libs/lib1/package.json', 'utf-8').toString() + ); + expect(distPackageJson).toMatchInlineSnapshot(` + Object { + "main": "./main.js", + "name": "@org/lib1", + "types": "./main.d.ts", + "version": "0.0.1", + } + `); + }); + + it('should keep package unchanged if "updateBuildableProjectDepsInPackageJson" not set', () => { + const fsJson = { + 'libs/lib1/package.json': JSON.stringify(originalPackageJson, null, 2), + }; + vol.fromJSON(fsJson, '/root'); + const options: UpdatePackageJsonOption = { + outputPath: 'dist/libs/lib1', + projectRoot: 'libs/lib1', + main: 'libs/lib1/main.ts', + }; + const dependencies: DependentBuildableProjectNode[] = []; + updatePackageJson(options, context, undefined, dependencies); + + expect(vol.existsSync('dist/libs/lib1/package.json')).toEqual(true); + const distPackageJson = JSON.parse( + vol.readFileSync('dist/libs/lib1/package.json', 'utf-8').toString() + ); + expect(distPackageJson.dependencies).toEqual( + originalPackageJson.dependencies + ); + expect(distPackageJson.main).toEqual('./main.js'); + expect(distPackageJson.types).toEqual('./main.d.ts'); + }); + + it('should modify package if "updateBuildableProjectDepsInPackageJson" is set', () => { + const fsJson = { + 'package.json': JSON.stringify(rootPackageJson, null, 2), + 'libs/lib1/package.json': JSON.stringify(originalPackageJson, null, 2), + }; + vol.fromJSON(fsJson, '/root'); + const options: UpdatePackageJsonOption = { + outputPath: 'dist/libs/lib1', + projectRoot: 'libs/lib1', + main: 'libs/lib1/main.ts', + updateBuildableProjectDepsInPackageJson: true, + }; + const dependencies: DependentBuildableProjectNode[] = []; + updatePackageJson(options, context, undefined, dependencies); + + expect(vol.existsSync('dist/libs/lib1/package.json')).toEqual(true); + const distPackageJson = JSON.parse( + vol.readFileSync('dist/libs/lib1/package.json', 'utf-8').toString() + ); + expect(distPackageJson).toMatchInlineSnapshot(` + Object { + "dependencies": Object { + "external1": "1.0.0", + "external2": "4.5.6", + }, + "main": "./main.js", + "name": "@org/lib1", + "types": "./main.d.ts", + "version": "0.0.1", + } + `); + }); +}); diff --git a/packages/js/src/utils/package-json/update-package-json.ts b/packages/js/src/utils/package-json/update-package-json.ts index 78083bb47d0c8..885309bda1ace 100644 --- a/packages/js/src/utils/package-json/update-package-json.ts +++ b/packages/js/src/utils/package-json/update-package-json.ts @@ -1,15 +1,20 @@ import { + createLockFile, + createPackageJson, ExecutorContext, + getOutputsForTargetAndConfiguration, + joinPathFragments, normalizePath, ProjectGraphProjectNode, readJsonFile, + workspaceRoot, writeJsonFile, } from '@nrwl/devkit'; -import { - DependentBuildableProjectNode, - updateBuildableProjectPackageJsonDependencies, -} from '@nrwl/workspace/src/utilities/buildable-libs-utils'; +import { DependentBuildableProjectNode } from '@nrwl/workspace/src/utilities/buildable-libs-utils'; import { basename, dirname, join, parse, relative } from 'path'; +import { getLockFileName } from 'nx/src/lock-file/lock-file'; +import { writeFileSync } from 'fs-extra'; +import { isNpmProject } from 'nx/src/project-graph/operators'; import { fileExists } from 'nx/src/utils/fileutils'; import type { PackageJson } from 'nx/src/utils/package-json'; @@ -36,6 +41,7 @@ export interface UpdatePackageJsonOption { excludeLibsInPackageJson?: boolean; updateBuildableProjectDepsInPackageJson?: boolean; buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies'; + generateLockfile?: boolean; } export function updatePackageJson( @@ -44,45 +50,107 @@ export function updatePackageJson( target: ProjectGraphProjectNode, dependencies: DependentBuildableProjectNode[] ): void { - const pathToPackageJson = join( - context.root, - options.projectRoot, - 'package.json' - ); - - const packageJson = fileExists(pathToPackageJson) - ? readJsonFile(pathToPackageJson) - : { name: context.projectName }; + let packageJson: PackageJson; - if (options.excludeLibsInPackageJson) { - dependencies = dependencies.filter((dep) => dep.node.type !== 'lib'); - } + if (options.updateBuildableProjectDepsInPackageJson) { + packageJson = createPackageJson(context.projectName, context.projectGraph, { + root: context.root, + // By default we remove devDependencies since this is a production build. + isProduction: true, + }); - writeJsonFile( - `${options.outputPath}/package.json`, - getUpdatedPackageJsonContent(packageJson, options) - ); + if (options.excludeLibsInPackageJson) { + dependencies = dependencies.filter((dep) => dep.node.type !== 'lib'); + } - if ( - dependencies.length > 0 && - options.updateBuildableProjectDepsInPackageJson - ) { - updateBuildableProjectPackageJsonDependencies( - context.root, - context.projectName, - context.targetName, - context.configurationName, - target, + addMissingDependencies( + packageJson, + context, dependencies, options.buildableProjectDepsInPackageJsonType ); + } else { + const pathToPackageJson = join( + context.root, + options.projectRoot, + 'package.json' + ); + packageJson = fileExists(pathToPackageJson) + ? readJsonFile(pathToPackageJson) + : { name: context.projectName, version: '0.0.1' }; + } + + // update package specific settings + packageJson = getUpdatedPackageJsonContent(packageJson, options); + + // save files + writeJsonFile(`${options.outputPath}/package.json`, packageJson); + + if (options.generateLockfile) { + const lockFile = createLockFile(packageJson); + writeFileSync(`${options.outputPath}/${getLockFileName()}`, lockFile, { + encoding: 'utf-8', + }); } } +function addMissingDependencies( + packageJson: PackageJson, + { projectName, targetName, configurationName, root }: ExecutorContext, + dependencies: DependentBuildableProjectNode[], + propType: 'dependencies' | 'peerDependencies' = 'dependencies' +) { + const workspacePackageJson = readJsonFile( + joinPathFragments(workspaceRoot, 'package.json') + ); + dependencies.forEach((entry) => { + if (isNpmProject(entry.node)) { + const { packageName, version } = entry.node.data; + if ( + packageJson.dependencies?.[packageName] || + packageJson.devDependencies?.[packageName] || + packageJson.peerDependencies?.[packageName] + ) { + return; + } + if (workspacePackageJson.devDependencies?.[packageName]) { + return; + } + + packageJson[propType] ??= {}; + packageJson[propType][packageName] = version; + } else { + const packageName = entry.name; + if ( + !packageJson.dependencies?.[packageName] && + !packageJson.peerDependencies?.[packageName] + ) { + const outputs = getOutputsForTargetAndConfiguration( + { + overrides: {}, + target: { + project: projectName, + target: targetName, + configuration: configurationName, + }, + }, + entry.node + ); + + const depPackageJsonPath = join(root, outputs[0], 'package.json'); + const version = readJsonFile(depPackageJsonPath).version; + + packageJson[propType] ??= {}; + packageJson[propType][packageName] = version; + } + } + }); +} + export function getUpdatedPackageJsonContent( packageJson: PackageJson, options: UpdatePackageJsonOption -) { +): PackageJson { // Default is CJS unless esm is explicitly passed. const hasCjsFormat = !options.format || options.format?.includes('cjs'); const hasEsmFormat = options.format?.includes('esm'); diff --git a/packages/js/src/utils/schema.d.ts b/packages/js/src/utils/schema.d.ts index 0d31037049c48..c09831472c11e 100644 --- a/packages/js/src/utils/schema.d.ts +++ b/packages/js/src/utils/schema.d.ts @@ -47,6 +47,7 @@ export interface ExecutorOptions { buildableProjectDepsInPackageJsonType?: 'dependencies' | 'peerDependencies'; external?: 'all' | 'none' | string[]; externalBuildTargets?: string[]; + generateLockfile?: boolean; } export interface NormalizedExecutorOptions extends ExecutorOptions {