Skip to content

Commit 182d680

Browse files
authored
Merge pull request #1495 from aldenquimby/fix-1493-alternative
Allow generating ESM output from npm (non-breaking)
2 parents d1bc6d7 + 0e373da commit 182d680

File tree

8 files changed

+136
-26
lines changed

8 files changed

+136
-26
lines changed

README.md

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -406,28 +406,46 @@ you should use any version `<5.5 >=5.7.1` as the versions in-between have some n
406406

407407
The NPM packager supports the following `packagerOptions`:
408408

409-
| Option | Type | Default | Description |
410-
| ------------------ | ------ | --------------------- | --------------------------------------------------- |
411-
| ignoreScripts | bool | false | Do not execute package.json hook scripts on install |
412-
| noInstall | bool | false | Do not run `npm install` (assume install completed) |
413-
| lockFile | string | ./package-lock.json | Relative path to lock file to use |
409+
| Option | Type | Default | Description |
410+
|-------------------------|----------|---------------------|---------------------------------------------------------------------|
411+
| ignoreScripts | bool | false | Do not execute package.json hook scripts on install |
412+
| noInstall | bool | false | Do not run `npm install` (assume install completed) |
413+
| lockFile | string | ./package-lock.json | Relative path to lock file to use |
414+
| copyPackageSectionNames | string[] | [] | Entries in your `package.json` to copy to the output `package.json` (ie: ESM output) |
414415

415416
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
416417
supports of NPM `>= 8.0.0` which installs `peer-dependencies` automatically. The plugin will be able to detect the correct version.
417418

419+
###### ESM output
420+
421+
If you need to generate ESM output, and you cannot safely produce a `.mjs` file
422+
(e.g. [because that breaks serverless-offline](https://github.com/serverless/serverless/issues/11308)),
423+
you can use `copyPackageSectionNames` to ensure the output `package.json` defaults to ESM.
424+
425+
```yaml
426+
custom:
427+
webpack:
428+
packagerOptions:
429+
copyPackageSectionNames:
430+
- type
431+
- exports
432+
- main
433+
```
434+
418435
##### Yarn
419436

420437
Using yarn will switch the whole packaging pipeline to use yarn, so does it use a `yarn.lock` file.
421438

422439
The yarn packager supports the following `packagerOptions`:
423440

424-
| Option | Type | Default | Description |
425-
| ------------------ | ---- | ------- | --------------------------------------------------- |
426-
| ignoreScripts | bool | false | Do not execute package.json hook scripts on install |
427-
| noInstall | bool | false | Do not run `yarn install` (assume install completed)|
428-
| noNonInteractive | bool | false | Disable interactive mode when using Yarn 2 or above |
429-
| noFrozenLockfile | bool | false | Do not require an up-to-date yarn.lock |
430-
| networkConcurrency | int | | Specify number of concurrent network requests |
441+
| Option | Type | Default | Description |
442+
|-------------------------|----------|-----------------|---------------------------------------------------------------------|
443+
| ignoreScripts | bool | false | Do not execute package.json hook scripts on install |
444+
| noInstall | bool | false | Do not run `yarn install` (assume install completed) |
445+
| noNonInteractive | bool | false | Disable interactive mode when using Yarn 2 or above |
446+
| noFrozenLockfile | bool | false | Do not require an up-to-date yarn.lock |
447+
| networkConcurrency | int | | Specify number of concurrent network requests |
448+
| copyPackageSectionNames | string[] | ['resolutions'] | Entries in your `package.json` to copy to the output `package.json` |
431449

432450
##### Common packager options
433451

lib/packExternalModules.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ module.exports = {
266266
// Determine and create packager
267267
return BbPromise.try(() => Packagers.get.call(this, this.configuration.packager)).then(packager => {
268268
// Fetch needed original package.json sections
269-
const sectionNames = packager.copyPackageSectionNames;
269+
const sectionNames = packager.copyPackageSectionNames(this.configuration.packagerOptions);
270270
const packageJson = this.serverless.utils.readFileSync(packageJsonPath);
271271
const packageSections = _.pick(packageJson, sectionNames);
272272
if (!_.isEmpty(packageSections)) {

lib/packagers/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
* interface Packager {
88
*
99
* static get lockfileName(): string;
10-
* static get copyPackageSectionNames(): Array<string>;
1110
* static get mustCopyModules(): boolean;
11+
* static copyPackageSectionNames(packagerOptions: Object): Array<string>;
1212
* static getPackagerVersion(cwd: string): BbPromise<Object>
1313
* static getProdDependencies(cwd: string, depth: number = 1): BbPromise<Object>;
1414
* static rebaseLockfile(pathToPackageRoot: string, lockfile: Object): void;

lib/packagers/npm.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ class NPM {
1616
return 'package-lock.json';
1717
}
1818

19-
static get copyPackageSectionNames() {
20-
return [];
21-
}
22-
2319
// eslint-disable-next-line lodash/prefer-constant
2420
static get mustCopyModules() {
2521
return true;
2622
}
2723

24+
static copyPackageSectionNames(packagerOptions) {
25+
const options = packagerOptions || {};
26+
return options.copyPackageSectionNames || [];
27+
}
28+
2829
static getPackagerVersion(cwd) {
2930
const command = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';
3031
const args = ['-v'];

lib/packagers/yarn.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ class Yarn {
2121
return 'yarn.lock';
2222
}
2323

24-
static get copyPackageSectionNames() {
25-
return ['resolutions'];
26-
}
27-
2824
// eslint-disable-next-line lodash/prefer-constant
2925
static get mustCopyModules() {
3026
return false;
3127
}
3228

29+
static copyPackageSectionNames(packagerOptions) {
30+
const options = packagerOptions || {};
31+
return options.copyPackageSectionNames || ['resolutions'];
32+
}
33+
3334
static isBerryVersion(version) {
3435
const versionNumber = version.charAt(0);
3536
const mainVersion = parseInt(versionNumber);

tests/packExternalModules.test.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jest.mock('fs-extra');
1818
jest.mock('../lib/packagers/index', () => {
1919
const packagerMock = {
2020
lockfileName: 'mocked-lock.json',
21-
copyPackageSectionNames: ['section1', 'section2'],
21+
copyPackageSectionNames: jest.requireActual('../lib/packagers/npm').copyPackageSectionNames,
2222
mustCopyModules: true,
2323
rebaseLockfile: jest.fn(),
2424
getPackagerVersion: jest.fn(),
@@ -196,6 +196,15 @@ describe('packExternalModules', () => {
196196
section1: originalPackageJSON.section1
197197
};
198198

199+
module.configuration = new Configuration({
200+
webpack: {
201+
includeModules: true,
202+
packager: 'npm',
203+
packagerOptions: {
204+
copyPackageSectionNames: ['section1', 'section2']
205+
}
206+
}
207+
});
199208
module.webpackOutputPath = '/my/Service/Path/outputPath';
200209
readFileSyncStub.mockReturnValueOnce(originalPackageJSON);
201210
readFileSyncStub.mockImplementation(() => {
@@ -229,6 +238,79 @@ describe('packExternalModules', () => {
229238
);
230239
});
231240

241+
it('should include ESM type from package.json according to packagerOptions', () => {
242+
const originalPackageJSON = {
243+
name: 'test-service',
244+
version: '1.0.0',
245+
description: 'Packaged externals for test-service',
246+
private: true,
247+
type: 'module',
248+
dependencies: {
249+
'@scoped/vendor': '1.0.0',
250+
bluebird: '^3.4.0',
251+
uuid: '^5.4.1'
252+
}
253+
};
254+
const expectedCompositePackageJSON = {
255+
name: 'test-service',
256+
version: '1.0.0',
257+
description: 'Packaged externals for test-service',
258+
private: true,
259+
scripts: {},
260+
type: 'module',
261+
dependencies: {
262+
'@scoped/vendor': '1.0.0',
263+
bluebird: '^3.4.0',
264+
uuid: '^5.4.1'
265+
}
266+
};
267+
const expectedPackageJSON = {
268+
name: 'test-service',
269+
version: '1.0.0',
270+
description: 'Packaged externals for test-service',
271+
private: true,
272+
scripts: {},
273+
dependencies: {
274+
'@scoped/vendor': '1.0.0',
275+
bluebird: '^3.4.0',
276+
uuid: '^5.4.1'
277+
},
278+
type: 'module'
279+
};
280+
module.configuration = new Configuration({
281+
webpack: {
282+
includeModules: true,
283+
packager: 'npm',
284+
packagerOptions: {
285+
copyPackageSectionNames: ['type']
286+
}
287+
}
288+
});
289+
290+
module.webpackOutputPath = '/my/Service/Path/outputPath';
291+
fsExtraMock.pathExists.mockImplementation((p, cb) => cb(null, true));
292+
fsExtraMock.copy.mockImplementation((from, to, cb) => cb());
293+
readFileSyncStub.mockReturnValueOnce(originalPackageJSON);
294+
readFileSyncStub.mockImplementation(() => {
295+
throw new Error('Unexpected call to readFileSync');
296+
});
297+
packagerFactoryMock.get('npm').rebaseLockfile.mockImplementation((pathToPackageRoot, lockfile) => lockfile);
298+
packagerFactoryMock.get('npm').getProdDependencies.mockReturnValue(BbPromise.resolve({}));
299+
packagerFactoryMock.get('npm').install.mockReturnValue(BbPromise.resolve());
300+
packagerFactoryMock.get('npm').prune.mockReturnValue(BbPromise.resolve());
301+
packagerFactoryMock.get('npm').runScripts.mockReturnValue(BbPromise.resolve());
302+
module.compileStats = stats;
303+
return expect(module.packExternalModules())
304+
.resolves.toBeUndefined()
305+
.then(() =>
306+
BbPromise.all([
307+
expect(writeFileSyncStub).toHaveBeenCalledTimes(2),
308+
expect(writeFileSyncStub.mock.calls[0][1]).toEqual(JSON.stringify(expectedCompositePackageJSON, null, 2)),
309+
expect(writeFileSyncStub.mock.calls[1][1]).toEqual(JSON.stringify(expectedPackageJSON, null, 2))
310+
])
311+
);
312+
});
313+
232314
it('should install external modules', () => {
233315
const expectedCompositePackageJSON = {
234316
name: 'test-service',

tests/packagers/npm.test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ describe('npm', () => {
3030
expect(npmModule.lockfileName).toEqual('package-lock.json');
3131
});
3232

33-
it('should return no packager sections', () => {
34-
expect(npmModule.copyPackageSectionNames).toEqual([]);
33+
it('should return no packager sections by default', () => {
34+
expect(npmModule.copyPackageSectionNames()).toEqual([]);
35+
});
36+
37+
it('should return packager sections from config', () => {
38+
expect(npmModule.copyPackageSectionNames({ copyPackageSectionNames: ['type'] })).toEqual(['type']);
3539
});
3640

3741
it('requires to copy modules', () => {

tests/packagers/yarn.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ describe('yarn', () => {
2121
});
2222

2323
it('should return packager sections', () => {
24-
expect(yarnModule.copyPackageSectionNames).toEqual(['resolutions']);
24+
expect(yarnModule.copyPackageSectionNames()).toEqual(['resolutions']);
25+
});
26+
27+
it('should return packager sections from config', () => {
28+
expect(yarnModule.copyPackageSectionNames({ copyPackageSectionNames: ['type'] })).toEqual(['type']);
2529
});
2630

2731
it('does not require to copy modules', () => {

0 commit comments

Comments
 (0)