fix(ngcc): support simple `browser` property in entry-points (#36396)

The `browser` package.json property is now supported to the same
level as `main` - i.e. it is sniffed for UMD, ESM5 and CommonJS.

The `browser` property can also contain an object with file overrides
but this is not supported by ngcc.

Fixes #36062

PR Close #36396
This commit is contained in:
Pete Bacon Darwin 2020-04-02 16:47:17 +01:00 committed by Kara Erickson
parent 2463548fa7
commit 6b3aa60446
3 changed files with 64 additions and 44 deletions

View File

@ -50,6 +50,7 @@ export interface JsonObject {
} }
export interface PackageJsonFormatPropertiesMap { export interface PackageJsonFormatPropertiesMap {
browser?: string;
fesm2015?: string; fesm2015?: string;
fesm5?: string; fesm5?: string;
es2015?: string; // if exists then it is actually FESM2015 es2015?: string; // if exists then it is actually FESM2015
@ -75,7 +76,7 @@ export interface EntryPointPackageJson extends JsonObject, PackageJsonFormatProp
export type EntryPointJsonProperty = Exclude<PackageJsonFormatProperties, 'types'|'typings'>; export type EntryPointJsonProperty = Exclude<PackageJsonFormatProperties, 'types'|'typings'>;
// We need to keep the elements of this const and the `EntryPointJsonProperty` type in sync. // We need to keep the elements of this const and the `EntryPointJsonProperty` type in sync.
export const SUPPORTED_FORMAT_PROPERTIES: EntryPointJsonProperty[] = export const SUPPORTED_FORMAT_PROPERTIES: EntryPointJsonProperty[] =
['fesm2015', 'fesm5', 'es2015', 'esm2015', 'esm5', 'main', 'module']; ['fesm2015', 'fesm5', 'es2015', 'esm2015', 'esm5', 'main', 'module', 'browser'];
/** /**
@ -193,13 +194,18 @@ export function getEntryPointFormat(
return 'esm2015'; return 'esm2015';
case 'esm5': case 'esm5':
return 'esm5'; return 'esm5';
case 'browser':
const browserFile = entryPoint.packageJson['browser'];
if (typeof browserFile !== 'string') {
return undefined;
}
return sniffModuleFormat(fs, join(entryPoint.path, browserFile));
case 'main': case 'main':
const mainFile = entryPoint.packageJson['main']; const mainFile = entryPoint.packageJson['main'];
if (mainFile === undefined) { if (mainFile === undefined) {
return undefined; return undefined;
} }
const pathToMain = join(entryPoint.path, mainFile); return sniffModuleFormat(fs, join(entryPoint.path, mainFile));
return sniffModuleFormat(fs, pathToMain);
case 'module': case 'module':
return 'esm5'; return 'esm5';
default: default:

View File

@ -793,7 +793,7 @@ runInEachFileSystem(() => {
const propertiesToConsider = ['es1337', 'fesm42']; const propertiesToConsider = ['es1337', 'fesm42'];
const errorMessage = const errorMessage =
'No supported format property to consider among [es1337, fesm42]. Supported ' + 'No supported format property to consider among [es1337, fesm42]. Supported ' +
'properties: fesm2015, fesm5, es2015, esm2015, esm5, main, module'; 'properties: fesm2015, fesm5, es2015, esm2015, esm5, main, module, browser';
expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider})) expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider}))
.toThrowError(errorMessage); .toThrowError(errorMessage);

View File

@ -10,7 +10,7 @@ import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../../
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {loadTestFiles} from '../../../test/helpers'; import {loadTestFiles} from '../../../test/helpers';
import {NgccConfiguration} from '../../src/packages/configuration'; import {NgccConfiguration} from '../../src/packages/configuration';
import {EntryPoint, getEntryPointFormat, getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point'; import {EntryPoint, EntryPointJsonProperty, getEntryPointFormat, getEntryPointInfo, INCOMPATIBLE_ENTRY_POINT, NO_ENTRY_POINT, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
runInEachFileSystem(() => { runInEachFileSystem(() => {
@ -206,7 +206,7 @@ runInEachFileSystem(() => {
for (let prop of SUPPORTED_FORMAT_PROPERTIES) { for (let prop of SUPPORTED_FORMAT_PROPERTIES) {
// Ignore the UMD format // Ignore the UMD format
if (prop === 'main') continue; if (prop === 'main' || prop === 'browser') continue;
// Let's give 'module' a specific path, otherwise compute it based on the property. // Let's give 'module' a specific path, otherwise compute it based on the property.
const typingsPath = prop === 'module' ? 'index' : `${prop}/missing_typings`; const typingsPath = prop === 'module' ? 'index' : `${prop}/missing_typings`;
@ -425,67 +425,80 @@ runInEachFileSystem(() => {
expect(getEntryPointFormat(fs, entryPoint, 'module')).toBe('esm5'); expect(getEntryPointFormat(fs, entryPoint, 'module')).toBe('esm5');
}); });
it('should return `esm5` for `main` if the file contains import or export statements', () => { (['browser', 'main'] as EntryPointJsonProperty[]).forEach(browserOrMain => {
const name = _( it('should return `esm5` for `' + browserOrMain +
'/project/node_modules/some_package/valid_entry_point/bundles/valid_entry_point/index.js'); '` if the file contains import or export statements',
loadTestFiles([{name, contents: `import * as core from '@angular/core;`}]); () => {
expect(getEntryPointFormat(fs, entryPoint, 'main')).toBe('esm5'); const name = _(
'/project/node_modules/some_package/valid_entry_point/bundles/valid_entry_point/index.js');
loadTestFiles([{name, contents: `import * as core from '@angular/core;`}]);
expect(getEntryPointFormat(fs, entryPoint, browserOrMain)).toBe('esm5');
loadTestFiles([{name, contents: `import {Component} from '@angular/core;`}]); loadTestFiles([{name, contents: `import {Component} from '@angular/core;`}]);
expect(getEntryPointFormat(fs, entryPoint, 'main')).toBe('esm5'); expect(getEntryPointFormat(fs, entryPoint, browserOrMain)).toBe('esm5');
loadTestFiles([{name, contents: `export function foo() {}`}]); loadTestFiles([{name, contents: `export function foo() {}`}]);
expect(getEntryPointFormat(fs, entryPoint, 'main')).toBe('esm5'); expect(getEntryPointFormat(fs, entryPoint, browserOrMain)).toBe('esm5');
loadTestFiles([{name, contents: `export * from 'abc';`}]); loadTestFiles([{name, contents: `export * from 'abc';`}]);
expect(getEntryPointFormat(fs, entryPoint, 'main')).toBe('esm5'); expect(getEntryPointFormat(fs, entryPoint, browserOrMain)).toBe('esm5');
}); });
it('should return `umd` for `main` if the file contains a UMD wrapper function', () => { it('should return `umd` for `' + browserOrMain +
loadTestFiles([{ '` if the file contains a UMD wrapper function',
name: _( () => {
'/project/node_modules/some_package/valid_entry_point/bundles/valid_entry_point/index.js'), loadTestFiles([{
contents: ` name: _(
'/project/node_modules/some_package/valid_entry_point/bundles/valid_entry_point/index.js'),
contents: `
(function (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) :
typeof define === 'function' && define.amd ? define('@angular/common', ['exports', '@angular/core'], factory) : typeof define === 'function' && define.amd ? define('@angular/common', ['exports', '@angular/core'], factory) :
(global = global || self, factory((global.ng = global.ng || {}, global.ng.common = {}), global.ng.core)); (global = global || self, factory((global.ng = global.ng || {}, global.ng.common = {}), global.ng.core));
}(this, function (exports, core) { 'use strict'; })); }(this, function (exports, core) { 'use strict'; }));
` `
}]); }]);
expect(getEntryPointFormat(fs, entryPoint, 'main')).toBe('umd'); expect(getEntryPointFormat(fs, entryPoint, browserOrMain)).toBe('umd');
}); });
it('should return `commonjs` for `main` if the file does not contain a UMD wrapper function', () => { it('should return `commonjs` for `' + browserOrMain +
loadTestFiles([{ '` if the file does not contain a UMD wrapper function',
name: _( () => {
'/project/node_modules/some_package/valid_entry_point/bundles/valid_entry_point/index.js'), loadTestFiles([{
contents: ` name: _(
'/project/node_modules/some_package/valid_entry_point/bundles/valid_entry_point/index.js'),
contents: `
const core = require('@angular/core); const core = require('@angular/core);
module.exports = {}; module.exports = {};
` `
}]); }]);
expect(getEntryPointFormat(fs, entryPoint, 'main')).toBe('commonjs'); expect(getEntryPointFormat(fs, entryPoint, browserOrMain)).toBe('commonjs');
}); });
it('should resolve the format path with suitable postfixes', () => { it('should resolve the format path with suitable postfixes', () => {
loadTestFiles([{ loadTestFiles([{
name: _( name: _(
'/project/node_modules/some_package/valid_entry_point/bundles/valid_entry_point/index.js'), '/project/node_modules/some_package/valid_entry_point/bundles/valid_entry_point/index.js'),
contents: ` contents: `
(function (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) :
typeof define === 'function' && define.amd ? define('@angular/common', ['exports', '@angular/core'], factory) : typeof define === 'function' && define.amd ? define('@angular/common', ['exports', '@angular/core'], factory) :
(global = global || self, factory((global.ng = global.ng || {}, global.ng.common = {}), global.ng.core)); (global = global || self, factory((global.ng = global.ng || {}, global.ng.common = {}), global.ng.core));
}(this, function (exports, core) { 'use strict'; })); }(this, function (exports, core) { 'use strict'; }));
` `
}]); }]);
entryPoint.packageJson.main = './bundles/valid_entry_point/index'; entryPoint.packageJson.main = './bundles/valid_entry_point/index';
expect(getEntryPointFormat(fs, entryPoint, 'main')).toBe('umd'); expect(getEntryPointFormat(fs, entryPoint, browserOrMain)).toBe('umd');
entryPoint.packageJson.main = './bundles/valid_entry_point'; entryPoint.packageJson.main = './bundles/valid_entry_point';
expect(getEntryPointFormat(fs, entryPoint, 'main')).toBe('umd'); expect(getEntryPointFormat(fs, entryPoint, browserOrMain)).toBe('umd');
});
});
it('should return `undefined` if the `browser` property is not a string', () => {
entryPoint.packageJson.browser = {} as any;
expect(getEntryPointFormat(fs, entryPoint, 'browser')).toBeUndefined();
}); });
}); });
}); });
@ -503,6 +516,7 @@ export function createPackageJson(
fesm5: `./fesm5/${packageName}.js`, fesm5: `./fesm5/${packageName}.js`,
esm5: `./esm5/${packageName}.js`, esm5: `./esm5/${packageName}.js`,
main: `./bundles/${packageName}/index.js`, main: `./bundles/${packageName}/index.js`,
browser: `./bundles/${packageName}/index.js`,
module: './index.js', module: './index.js',
}; };
if (excludes) { if (excludes) {