Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow generating ESM output from npm (non-breaking) #1495

Merged
merged 1 commit into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
vicary marked this conversation as resolved.
Show resolved Hide resolved
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