Skip to content

Commit

Permalink
Merge pull request #1495 from aldenquimby/fix-1493-alternative
Browse files Browse the repository at this point in the history
Allow generating ESM output from npm (non-breaking)
  • Loading branch information
j0k3r authored Jul 25, 2023
2 parents d1bc6d7 + 0e373da commit 182d680
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 26 deletions.
42 changes: 30 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/packExternalModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion lib/packagers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* interface Packager {
*
* static get lockfileName(): string;
* static get copyPackageSectionNames(): Array<string>;
* static get mustCopyModules(): boolean;
* static copyPackageSectionNames(packagerOptions: Object): Array<string>;
* static getPackagerVersion(cwd: string): BbPromise<Object>
* static getProdDependencies(cwd: string, depth: number = 1): BbPromise<Object>;
* static rebaseLockfile(pathToPackageRoot: string, lockfile: Object): void;
Expand Down
9 changes: 5 additions & 4 deletions lib/packagers/npm.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
9 changes: 5 additions & 4 deletions lib/packagers/yarn.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
84 changes: 83 additions & 1 deletion tests/packExternalModules.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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',
Expand Down
8 changes: 6 additions & 2 deletions tests/packagers/npm.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
6 changes: 5 additions & 1 deletion tests/packagers/yarn.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down

0 comments on commit 182d680

Please sign in to comment.