From 0e373da015bbaa10ab69d9b1d6314d9472fbe28f Mon Sep 17 00:00:00 2001 From: Alden Quimby Date: Wed, 7 Jun 2023 10:52:30 -0400 Subject: [PATCH] Allow generating ESM output from npm --- README.md | 42 +++++++++++----- lib/packExternalModules.js | 2 +- lib/packagers/index.js | 2 +- lib/packagers/npm.js | 9 ++-- lib/packagers/yarn.js | 9 ++-- tests/packExternalModules.test.js | 84 ++++++++++++++++++++++++++++++- tests/packagers/npm.test.js | 8 ++- tests/packagers/yarn.test.js | 6 ++- 8 files changed, 136 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 680ba3e96..9f78cc5b3 100644 --- a/README.md +++ b/README.md @@ -406,28 +406,46 @@ you should use any version `<5.5 >=5.7.1` as the versions in-between have some n The NPM packager supports the following `packagerOptions`: -| Option | Type | Default | Description | -| ------------------ | ------ | --------------------- | --------------------------------------------------- | -| ignoreScripts | bool | false | Do not execute package.json hook scripts on install | -| noInstall | bool | false | Do not run `npm install` (assume install completed) | -| lockFile | string | ./package-lock.json | Relative path to lock file to use | +| Option | Type | Default | Description | +|-------------------------|----------|---------------------|---------------------------------------------------------------------| +| ignoreScripts | bool | false | Do not execute package.json hook scripts on install | +| noInstall | bool | false | Do not run `npm install` (assume install completed) | +| lockFile | string | ./package-lock.json | Relative path to lock file to use | +| copyPackageSectionNames | string[] | [] | Entries in your `package.json` to copy to the output `package.json` (ie: ESM output) | When using NPM version `>= 7.0.0`, we will use the `package-lock.json` file instead of modules installed in `node_modules`. This improves the supports of NPM `>= 8.0.0` which installs `peer-dependencies` automatically. The plugin will be able to detect the correct version. +###### ESM output + +If you need to generate ESM output, and you cannot safely produce a `.mjs` file +(e.g. [because that breaks serverless-offline](https://github.com/serverless/serverless/issues/11308)), +you can use `copyPackageSectionNames` to ensure the output `package.json` defaults to ESM. + +```yaml +custom: + webpack: + packagerOptions: + copyPackageSectionNames: + - type + - exports + - main +``` + ##### Yarn Using yarn will switch the whole packaging pipeline to use yarn, so does it use a `yarn.lock` file. The yarn packager supports the following `packagerOptions`: -| Option | Type | Default | Description | -| ------------------ | ---- | ------- | --------------------------------------------------- | -| ignoreScripts | bool | false | Do not execute package.json hook scripts on install | -| noInstall | bool | false | Do not run `yarn install` (assume install completed)| -| noNonInteractive | bool | false | Disable interactive mode when using Yarn 2 or above | -| noFrozenLockfile | bool | false | Do not require an up-to-date yarn.lock | -| networkConcurrency | int | | Specify number of concurrent network requests | +| Option | Type | Default | Description | +|-------------------------|----------|-----------------|---------------------------------------------------------------------| +| ignoreScripts | bool | false | Do not execute package.json hook scripts on install | +| noInstall | bool | false | Do not run `yarn install` (assume install completed) | +| noNonInteractive | bool | false | Disable interactive mode when using Yarn 2 or above | +| noFrozenLockfile | bool | false | Do not require an up-to-date yarn.lock | +| networkConcurrency | int | | Specify number of concurrent network requests | +| copyPackageSectionNames | string[] | ['resolutions'] | Entries in your `package.json` to copy to the output `package.json` | ##### Common packager options diff --git a/lib/packExternalModules.js b/lib/packExternalModules.js index 7f4c4e3b9..64ccaa100 100644 --- a/lib/packExternalModules.js +++ b/lib/packExternalModules.js @@ -266,7 +266,7 @@ module.exports = { // Determine and create packager return BbPromise.try(() => Packagers.get.call(this, this.configuration.packager)).then(packager => { // Fetch needed original package.json sections - const sectionNames = packager.copyPackageSectionNames; + const sectionNames = packager.copyPackageSectionNames(this.configuration.packagerOptions); const packageJson = this.serverless.utils.readFileSync(packageJsonPath); const packageSections = _.pick(packageJson, sectionNames); if (!_.isEmpty(packageSections)) { diff --git a/lib/packagers/index.js b/lib/packagers/index.js index 4bea331fb..68e343416 100644 --- a/lib/packagers/index.js +++ b/lib/packagers/index.js @@ -7,8 +7,8 @@ * interface Packager { * * static get lockfileName(): string; - * static get copyPackageSectionNames(): Array; * static get mustCopyModules(): boolean; + * static copyPackageSectionNames(packagerOptions: Object): Array; * static getPackagerVersion(cwd: string): BbPromise * static getProdDependencies(cwd: string, depth: number = 1): BbPromise; * static rebaseLockfile(pathToPackageRoot: string, lockfile: Object): void; diff --git a/lib/packagers/npm.js b/lib/packagers/npm.js index d96a0d807..5d657ef20 100644 --- a/lib/packagers/npm.js +++ b/lib/packagers/npm.js @@ -16,15 +16,16 @@ class NPM { return 'package-lock.json'; } - static get copyPackageSectionNames() { - return []; - } - // eslint-disable-next-line lodash/prefer-constant static get mustCopyModules() { return true; } + static copyPackageSectionNames(packagerOptions) { + const options = packagerOptions || {}; + return options.copyPackageSectionNames || []; + } + static getPackagerVersion(cwd) { const command = /^win/.test(process.platform) ? 'npm.cmd' : 'npm'; const args = ['-v']; diff --git a/lib/packagers/yarn.js b/lib/packagers/yarn.js index 942c3dd1c..96884e03a 100644 --- a/lib/packagers/yarn.js +++ b/lib/packagers/yarn.js @@ -21,15 +21,16 @@ class Yarn { return 'yarn.lock'; } - static get copyPackageSectionNames() { - return ['resolutions']; - } - // eslint-disable-next-line lodash/prefer-constant static get mustCopyModules() { return false; } + static copyPackageSectionNames(packagerOptions) { + const options = packagerOptions || {}; + return options.copyPackageSectionNames || ['resolutions']; + } + static isBerryVersion(version) { const versionNumber = version.charAt(0); const mainVersion = parseInt(versionNumber); diff --git a/tests/packExternalModules.test.js b/tests/packExternalModules.test.js index e308399c1..eadc56907 100644 --- a/tests/packExternalModules.test.js +++ b/tests/packExternalModules.test.js @@ -18,7 +18,7 @@ jest.mock('fs-extra'); jest.mock('../lib/packagers/index', () => { const packagerMock = { lockfileName: 'mocked-lock.json', - copyPackageSectionNames: ['section1', 'section2'], + copyPackageSectionNames: jest.requireActual('../lib/packagers/npm').copyPackageSectionNames, mustCopyModules: true, rebaseLockfile: jest.fn(), getPackagerVersion: jest.fn(), @@ -196,6 +196,15 @@ describe('packExternalModules', () => { section1: originalPackageJSON.section1 }; + module.configuration = new Configuration({ + webpack: { + includeModules: true, + packager: 'npm', + packagerOptions: { + copyPackageSectionNames: ['section1', 'section2'] + } + } + }); module.webpackOutputPath = '/my/Service/Path/outputPath'; readFileSyncStub.mockReturnValueOnce(originalPackageJSON); readFileSyncStub.mockImplementation(() => { @@ -229,6 +238,79 @@ describe('packExternalModules', () => { ); }); + it('should include ESM type from package.json according to packagerOptions', () => { + const originalPackageJSON = { + name: 'test-service', + version: '1.0.0', + description: 'Packaged externals for test-service', + private: true, + type: 'module', + dependencies: { + '@scoped/vendor': '1.0.0', + bluebird: '^3.4.0', + uuid: '^5.4.1' + } + }; + const expectedCompositePackageJSON = { + name: 'test-service', + version: '1.0.0', + description: 'Packaged externals for test-service', + private: true, + scripts: {}, + type: 'module', + dependencies: { + '@scoped/vendor': '1.0.0', + bluebird: '^3.4.0', + uuid: '^5.4.1' + } + }; + const expectedPackageJSON = { + name: 'test-service', + version: '1.0.0', + description: 'Packaged externals for test-service', + private: true, + scripts: {}, + dependencies: { + '@scoped/vendor': '1.0.0', + bluebird: '^3.4.0', + uuid: '^5.4.1' + }, + type: 'module' + }; + module.configuration = new Configuration({ + webpack: { + includeModules: true, + packager: 'npm', + packagerOptions: { + copyPackageSectionNames: ['type'] + } + } + }); + + module.webpackOutputPath = '/my/Service/Path/outputPath'; + fsExtraMock.pathExists.mockImplementation((p, cb) => cb(null, true)); + fsExtraMock.copy.mockImplementation((from, to, cb) => cb()); + readFileSyncStub.mockReturnValueOnce(originalPackageJSON); + readFileSyncStub.mockImplementation(() => { + throw new Error('Unexpected call to readFileSync'); + }); + packagerFactoryMock.get('npm').rebaseLockfile.mockImplementation((pathToPackageRoot, lockfile) => lockfile); + packagerFactoryMock.get('npm').getProdDependencies.mockReturnValue(BbPromise.resolve({})); + packagerFactoryMock.get('npm').install.mockReturnValue(BbPromise.resolve()); + packagerFactoryMock.get('npm').prune.mockReturnValue(BbPromise.resolve()); + packagerFactoryMock.get('npm').runScripts.mockReturnValue(BbPromise.resolve()); + module.compileStats = stats; + return expect(module.packExternalModules()) + .resolves.toBeUndefined() + .then(() => + BbPromise.all([ + expect(writeFileSyncStub).toHaveBeenCalledTimes(2), + expect(writeFileSyncStub.mock.calls[0][1]).toEqual(JSON.stringify(expectedCompositePackageJSON, null, 2)), + expect(writeFileSyncStub.mock.calls[1][1]).toEqual(JSON.stringify(expectedPackageJSON, null, 2)) + ]) + ); + }); + it('should install external modules', () => { const expectedCompositePackageJSON = { name: 'test-service', diff --git a/tests/packagers/npm.test.js b/tests/packagers/npm.test.js index 9c2f185c6..7aa164d28 100644 --- a/tests/packagers/npm.test.js +++ b/tests/packagers/npm.test.js @@ -30,8 +30,12 @@ describe('npm', () => { expect(npmModule.lockfileName).toEqual('package-lock.json'); }); - it('should return no packager sections', () => { - expect(npmModule.copyPackageSectionNames).toEqual([]); + it('should return no packager sections by default', () => { + expect(npmModule.copyPackageSectionNames()).toEqual([]); + }); + + it('should return packager sections from config', () => { + expect(npmModule.copyPackageSectionNames({ copyPackageSectionNames: ['type'] })).toEqual(['type']); }); it('requires to copy modules', () => { diff --git a/tests/packagers/yarn.test.js b/tests/packagers/yarn.test.js index 2f483defb..ff1a0dff1 100644 --- a/tests/packagers/yarn.test.js +++ b/tests/packagers/yarn.test.js @@ -21,7 +21,11 @@ describe('yarn', () => { }); it('should return packager sections', () => { - expect(yarnModule.copyPackageSectionNames).toEqual(['resolutions']); + expect(yarnModule.copyPackageSectionNames()).toEqual(['resolutions']); + }); + + it('should return packager sections from config', () => { + expect(yarnModule.copyPackageSectionNames({ copyPackageSectionNames: ['type'] })).toEqual(['type']); }); it('does not require to copy modules', () => {