mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
1550663b9e
The `ng_module` rule supports the generation of flat module bundles. In View Engine, information about this flat module bundle is exposed as a Bazel provider. This is helpful as other rules like `ng_package` could rely on this information to determine entry-points for the APF. With Ivy this currently does not work because the flat module information is not exposed in the provider. The reason for this is unclear. We should also provide this information in Ivy so that rules like `ng_package` can also determine the correct entry-points when a package is built specifically with `--config=ivy`. PR Close #36971
488 lines
19 KiB
TypeScript
488 lines
19 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as shx from 'shelljs';
|
|
|
|
function main(args: string[]): number {
|
|
// Exit immediately when encountering an error.
|
|
shx.set('-e');
|
|
|
|
// Keep track of whether an error has occured so that we can return an appropriate exit code.
|
|
let errorHasOccured = false;
|
|
|
|
// This utility expects all of its arguments to be specified in a params file generated by
|
|
// bazel (see https://docs.bazel.build/versions/master/skylark/lib/Args.html#use_param_file).
|
|
const paramFilePath = args[0];
|
|
|
|
// Bazel params may be surrounded with quotes
|
|
function unquoteParameter(s: string) {
|
|
return s.replace(/^'(.*)'$/, '$1');
|
|
}
|
|
|
|
// Parameters are specified in the file one per line.
|
|
const params = fs.readFileSync(paramFilePath, 'utf-8').split('\n').map(unquoteParameter);
|
|
|
|
const [
|
|
// Output directory for the npm package.
|
|
out,
|
|
|
|
// The package segment of the ng_package rule's label (e.g. 'package/common').
|
|
srcDir,
|
|
|
|
// The bazel-bin dir joined with the srcDir (e.g. 'bazel-bin/package.common').
|
|
// This is the intended output location for package artifacts.
|
|
binDir,
|
|
|
|
// The bazel-genfiles dir joined with the srcDir (e.g. 'bazel-bin/package.common').
|
|
genfilesDir,
|
|
|
|
// JSON data mapping each entry point to the generated bundle index and
|
|
// flat module metadata, for example
|
|
// {"@angular/core": {
|
|
// "index": "bazel-bin/packages/core/core.js",
|
|
// "typings": "bazel-bin/packages/core/core.d.ts",
|
|
// "metadata": "bazel-bin/packages/core/core.metadata.json"
|
|
// },
|
|
// ...
|
|
// }
|
|
modulesManifestArg,
|
|
|
|
// Path to the package's README.md.
|
|
readmeMd,
|
|
|
|
// List of rolled-up flat ES2015 modules
|
|
fesm2015Arg,
|
|
|
|
// List of individual ES2015 modules
|
|
esm2015Arg,
|
|
|
|
// List of all UMD bundles generated by rollup.
|
|
bundlesArg,
|
|
|
|
// List of all files in the ng_package rule's srcs.
|
|
srcsArg,
|
|
|
|
// List of all type definitions that need to packaged into the ng_package.
|
|
typeDefinitionsArg,
|
|
|
|
// List of all files in the ng_package rule's data.
|
|
dataArg,
|
|
|
|
// Path to the package's LICENSE.
|
|
licenseFile,
|
|
|
|
// List of all dts bundles generated by the API extractor.
|
|
dtsBundleArg,
|
|
|
|
// The dts bundle file suffix example: '.bundle.d.ts'
|
|
dtsBundleFileSuffix,
|
|
] = params;
|
|
|
|
const fesm2015 = fesm2015Arg.split(',').filter(s => !!s);
|
|
const esm2015 = esm2015Arg.split(',').filter(s => !!s);
|
|
const bundles = bundlesArg.split(',').filter(s => !!s);
|
|
const typeDefinitions = typeDefinitionsArg.split(',').filter(s => !!s);
|
|
const srcs = srcsArg.split(',').filter(s => !!s);
|
|
const dataFiles: string[] = dataArg.split(',').filter(s => !!s);
|
|
const modulesManifest = JSON.parse(modulesManifestArg);
|
|
const dtsBundles: string[] = dtsBundleArg.split(',').filter(s => !!s);
|
|
|
|
/**
|
|
* List of known `package.json` fields which provide information about
|
|
* supported package formats and their associated entry paths.
|
|
*/
|
|
const knownFormatPackageJsonFields =
|
|
['main', 'fesm2015', 'esm2015', 'typings', 'module', 'es2015'];
|
|
|
|
if (readmeMd) {
|
|
copyFile(readmeMd, out);
|
|
}
|
|
|
|
/**
|
|
* Writes a file into the package based on its input path, relativizing to the package path.
|
|
* @param inputPath Path to the file in the input tree.
|
|
* @param fileContent Content of the file.
|
|
*/
|
|
function writeFileFromInputPath(inputPath: string, fileContent: string|Buffer) {
|
|
// We want the relative path from the given file to its ancestor "root" directory.
|
|
// This root depends on whether the file lives in the source tree (srcDir) as a basic file
|
|
// input to ng_package, the bin output tree (binDir) as the output of another rule, or
|
|
// the genfiles output tree (genfilesDir) as the output of a genrule.
|
|
let rootDir: string;
|
|
if (inputPath.includes(binDir)) {
|
|
rootDir = binDir;
|
|
} else if (inputPath.includes(genfilesDir)) {
|
|
rootDir = genfilesDir;
|
|
} else {
|
|
rootDir = srcDir;
|
|
}
|
|
|
|
const outputPath = path.join(out, path.relative(rootDir, inputPath));
|
|
|
|
// Always ensure that the target directory exists.
|
|
shx.mkdir('-p', path.dirname(outputPath));
|
|
fs.writeFileSync(outputPath, fileContent);
|
|
}
|
|
|
|
/**
|
|
* Copies a file into the package based on its input path, relativizing to the package path.
|
|
* @param inputPath a path relative to the binDir, typically from a file in the deps[]
|
|
*/
|
|
function copyFileFromInputPath(inputPath: string) {
|
|
writeFileFromInputPath(inputPath, fs.readFileSync(inputPath));
|
|
}
|
|
|
|
/**
|
|
* Relativize the path where a file is written.
|
|
* @param file a path containing a re-rooted segment like `.esm2015`
|
|
* @param outDir path where we copy the file, relative to the out
|
|
*/
|
|
function writeEsmFile(file: string, outDir: string) {
|
|
// Path computed relative to the current package in bazel-bin. e.g. a ES2015 output file
|
|
// in `bazel-out/<..>/packages/core/src/di.js` should be stored in `{out_dir}/src/di.js`
|
|
// if the package target has been declared in `<..>/packages/core`.
|
|
const packageRelativePath = path.dirname(path.relative(binDir, file));
|
|
if (!packageRelativePath.startsWith('..')) {
|
|
copyFile(file, path.join(out, outDir), packageRelativePath);
|
|
}
|
|
}
|
|
|
|
esm2015.forEach(file => writeEsmFile(file, 'esm2015'));
|
|
|
|
bundles.forEach(bundle => {
|
|
copyFile(bundle, out, 'bundles');
|
|
});
|
|
fesm2015.forEach(file => {
|
|
copyFile(file, out, 'fesm2015');
|
|
});
|
|
|
|
// Copy all type definitions into the package. This is necessary so that developers can use
|
|
// the package with type definitions.
|
|
typeDefinitions.forEach(f => writeFileFromInputPath(f, readTypingsAndStripAmdModule(f)));
|
|
|
|
// Copy all `data` files into the package. These are files that aren't built by the ng_package
|
|
// rule, but instead are just straight copied into the package, e.g. global CSS assets.
|
|
dataFiles.forEach(f => copyFileFromInputPath(f));
|
|
|
|
// Iterate through the entry point modules
|
|
// We do this first because we also record new paths for the esm5 and esm2015 copies
|
|
// of the index JS file, which we need to amend the package.json.
|
|
Object.keys(modulesManifest).forEach(moduleName => {
|
|
const moduleFiles = modulesManifest[moduleName];
|
|
const relative = path.relative(binDir, moduleFiles['index']);
|
|
|
|
moduleFiles['esm2015_index'] = path.join(binDir, 'esm2015', relative);
|
|
|
|
// Metadata file is optional as entry-points can be built with the `ts_library`
|
|
// rule or `ng_module` rule in Ivy mode.
|
|
const metadataFile = moduleFiles['metadata'];
|
|
if (!metadataFile) {
|
|
return;
|
|
}
|
|
|
|
const typingsOutFile = moduleFiles['typings'];
|
|
// We only support all modules within a package to be dts bundled
|
|
// ie: if @angular/common/http has flat dts, so should @angular/common
|
|
if (dtsBundles.length) {
|
|
const metadataContent = rewireMetadata(metadataFile, typingsOutFile);
|
|
writeFileFromInputPath(metadataFile, metadataContent);
|
|
} else {
|
|
copyFileFromInputPath(metadataFile);
|
|
}
|
|
});
|
|
|
|
const licenseBanner = licenseFile ? fs.readFileSync(licenseFile, 'utf-8') : '';
|
|
|
|
dtsBundles.forEach(bundleFile => {
|
|
const cleanDistPath = bundleFile.replace(dtsBundleFileSuffix, '.d.ts');
|
|
// API extractor will not dedupe license comments from various files
|
|
// this will remove all the license comments and append the license banner.
|
|
const content = licenseBanner + '\n' +
|
|
readTypingsAndStripAmdModule(bundleFile)
|
|
.replace(/(\/\*\*\s+\*\s\@license(((?!\*\/).|\s)*)\*\/)/gm, '');
|
|
|
|
writeFileFromInputPath(cleanDistPath, content);
|
|
});
|
|
|
|
// Root package name (e.g. '@angular/common'), captures as we iterate through sources below.
|
|
let rootPackageName = '';
|
|
const packagesWithExistingPackageJson = new Set<string>();
|
|
|
|
for (const src of srcs) {
|
|
if (src.includes(binDir) || src.includes(genfilesDir)) {
|
|
errorHasOccured = true;
|
|
console.error(
|
|
'The "srcs" for ng_package should not include output of other rules. Found:\n' +
|
|
` ${src}`);
|
|
}
|
|
|
|
let content = fs.readFileSync(src, 'utf-8');
|
|
// Modify package.json files as necessary for publishing
|
|
if (path.basename(src) === 'package.json') {
|
|
const packageJson = JSON.parse(content);
|
|
content = amendPackageJson(src, packageJson, false);
|
|
|
|
const packageName = packageJson['name'];
|
|
packagesWithExistingPackageJson.add(packageName);
|
|
|
|
// Keep track of the root package name, e.g. "@angular/common". We assume that the
|
|
// root name will be shortest because secondary entry-points will append to it
|
|
// (e.g. "@angular/common/http").
|
|
if (!rootPackageName || packageName.length < rootPackageName.length) {
|
|
rootPackageName = packageJson['name'];
|
|
}
|
|
}
|
|
writeFileFromInputPath(src, content);
|
|
}
|
|
|
|
// Generate extra files for secondary entry-points.
|
|
Object.keys(modulesManifest).forEach(entryPointPackageName => {
|
|
const entryPointName = entryPointPackageName.substr(rootPackageName.length + 1);
|
|
if (!entryPointName) return;
|
|
|
|
const metadataFilePath = modulesManifest[entryPointPackageName]['metadata'];
|
|
if (metadataFilePath) {
|
|
createMetadataReexportFile(
|
|
entryPointName, modulesManifest[entryPointPackageName]['metadata'],
|
|
entryPointPackageName);
|
|
}
|
|
|
|
createTypingsReexportFile(
|
|
entryPointName, licenseBanner, modulesManifest[entryPointPackageName]['typings']);
|
|
|
|
if (!packagesWithExistingPackageJson.has(entryPointPackageName)) {
|
|
createEntryPointPackageJson(entryPointName, entryPointPackageName);
|
|
}
|
|
});
|
|
|
|
return errorHasOccured ? 1 : 0;
|
|
|
|
/**
|
|
* Convert a binDir-relative path to srcDir-relative
|
|
* @param from path to a file under the srcDir, like packages/core/testing/package.json
|
|
* @param file path to a file under the binDir, like bazel-bin/core/testing/generated.js
|
|
*/
|
|
function srcDirRelative(from: string, file: string) {
|
|
const result = normalizeSeparators(
|
|
path.relative(path.dirname(from), path.join(srcDir, path.relative(binDir, file))));
|
|
if (result.startsWith('..')) return result;
|
|
return `./${result}`;
|
|
}
|
|
|
|
function copyFile(file: string, baseDir: string, relative = '.') {
|
|
const dir = path.join(baseDir, relative);
|
|
// output file is .js if the input file is .mjs
|
|
const outFile = path.posix.join(
|
|
dir, path.basename(file.endsWith('.mjs') ? file.replace(/\.mjs$/, '.js') : file));
|
|
shx.mkdir('-p', dir);
|
|
shx.cp(file, outFile);
|
|
// Double-underscore is used to escape forward slash in FESM filenames.
|
|
// See ng_package.bzl:
|
|
// fesm_output_filename = entry_point.replace("/", "__")
|
|
// We need to unescape these.
|
|
if (outFile.indexOf('__') >= 0) {
|
|
const outputPath = path.join(dir, ...path.basename(outFile).split('__'));
|
|
shx.mkdir('-p', path.dirname(outputPath));
|
|
shx.mv(path.join(dir, path.basename(file)), outputPath);
|
|
|
|
// if we are renaming the .js file, we'll also need to update the sourceMappingURL in the file
|
|
if (outFile.endsWith('.js')) {
|
|
shx.chmod('+w', outputPath);
|
|
shx.sed('-i', `${path.basename(file)}.map`, `${path.basename(outputPath)}.map`, outputPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inserts or edits properties into the package.json file(s) in the package so that
|
|
* they point to all the right generated artifacts.
|
|
*
|
|
* @param packageJson The path to the package.json file.
|
|
* @param parsedPackage Parsed package.json content
|
|
* @param isGeneratedPackageJson Whether the passed package.json has been generated.
|
|
*/
|
|
function amendPackageJson(
|
|
packageJson: string, parsedPackage: {[key: string]: string},
|
|
isGeneratedPackageJson: boolean) {
|
|
const packageName = parsedPackage['name'];
|
|
const moduleData = modulesManifest[packageName];
|
|
|
|
// If a package json file has been discovered that does not match any
|
|
// module in the manifest, we report a warning as most likely the target
|
|
// is configured incorrectly (e.g. missing `module_name` attribute).
|
|
if (!moduleData) {
|
|
// Ideally we should throw here, as we got an entry point that doesn't
|
|
// have flat module metadata / bundle index, so it may have been an
|
|
// ng_module that's missing a module_name attribute.
|
|
// However, @angular/compiler can't be an ng_module, as it's the internals
|
|
// of the ngc compiler, yet we want to build an ng_package for it.
|
|
// So ignore package.json files when we are missing data.
|
|
console.error('WARNING: no module metadata for package', packageName);
|
|
console.error(' Not updating the package.json file to point to it');
|
|
console.error(
|
|
' The ng_module for this package is possibly missing the module_name attribute ');
|
|
return JSON.stringify(parsedPackage, null, 2);
|
|
}
|
|
|
|
// If we guessed the index paths for a module, and it contains an explicit `package.json`
|
|
// file that already sets format properties, we skip automatic insertion of format
|
|
// properties but report a warning in case properties have been set by accident.
|
|
if (moduleData.guessedPaths && !isGeneratedPackageJson &&
|
|
hasExplicitFormatProperties(parsedPackage)) {
|
|
console.error('WARNING: `package.json` explicitly sets format properties (like `main`).');
|
|
console.error(
|
|
' Skipping automatic insertion of format properties as explicit ' +
|
|
'format properties are set.');
|
|
console.error(' Ignore this warning if explicit properties are set intentionally.');
|
|
return JSON.stringify(parsedPackage, null, 2);
|
|
}
|
|
|
|
// Derive the paths to the files from the hard-coded names we gave them.
|
|
// TODO(alexeagle): it would be better to transfer this information from the place
|
|
// where we created the filenames, via the modulesManifestArg
|
|
parsedPackage['main'] = getBundleName(packageName, 'bundles');
|
|
parsedPackage['fesm2015'] = getBundleName(packageName, 'fesm2015');
|
|
|
|
parsedPackage['esm2015'] = srcDirRelative(packageJson, moduleData['esm2015_index']);
|
|
parsedPackage['typings'] = srcDirRelative(packageJson, moduleData['typings']);
|
|
|
|
// For now, we point the primary entry points at the fesm files, because of Webpack
|
|
// performance issues with a large number of individual files.
|
|
parsedPackage['module'] = parsedPackage['fesm2015'];
|
|
parsedPackage['es2015'] = parsedPackage['fesm2015'];
|
|
|
|
return JSON.stringify(parsedPackage, null, 2);
|
|
}
|
|
|
|
// e.g. @angular/common/http/testing -> ../../bundles/common-http-testing.umd.js
|
|
// or @angular/common/http/testing -> ../../fesm2015/http/testing.js
|
|
function getBundleName(packageName: string, dir: string) {
|
|
const parts = packageName.split('/');
|
|
// Remove the scoped package part, like @angular if present
|
|
const nameParts = packageName.startsWith('@') ? parts.splice(1) : parts;
|
|
const relativePath = newArray(nameParts.length - 1, '..').join('/') || '.';
|
|
let basename: string;
|
|
if (dir === 'bundles') {
|
|
basename = nameParts.join('-') + '.umd';
|
|
} else if (nameParts.length === 1) {
|
|
basename = nameParts[0];
|
|
} else {
|
|
basename = nameParts.slice(1).join('/');
|
|
}
|
|
return [relativePath, dir, basename + '.js'].join('/');
|
|
}
|
|
|
|
/** Whether the package explicitly sets any of the format properties (like `main`). */
|
|
function hasExplicitFormatProperties(parsedPackage: {[key: string]: string}): boolean {
|
|
return Object.keys(parsedPackage)
|
|
.some(propertyName => knownFormatPackageJsonFields.includes(propertyName));
|
|
}
|
|
|
|
/** Creates metadata re-export file for a secondary entry-point. */
|
|
function createMetadataReexportFile(
|
|
entryPointName: string, metadataFile: string, packageName: string) {
|
|
const inputPath = path.join(srcDir, `${entryPointName}.metadata.json`);
|
|
writeFileFromInputPath(inputPath, JSON.stringify({
|
|
'__symbolic': 'module',
|
|
'version': 3,
|
|
'metadata': {},
|
|
'exports':
|
|
[{'from': `${srcDirRelative(inputPath, metadataFile.replace(/.metadata.json$/, ''))}`}],
|
|
'flatModuleIndexRedirect': true,
|
|
'importAs': packageName
|
|
}) + '\n');
|
|
}
|
|
|
|
/**
|
|
* Creates a typings (d.ts) re-export file for a secondary-entry point,
|
|
* e.g., `export * from './common/common'`
|
|
*/
|
|
function createTypingsReexportFile(entryPointName: string, license: string, typingsFile: string) {
|
|
const inputPath = path.join(srcDir, `${entryPointName}.d.ts`);
|
|
const content = `${license}
|
|
export * from '${srcDirRelative(inputPath, typingsFile.replace(/\.d\.tsx?$/, ''))}';
|
|
`;
|
|
writeFileFromInputPath(inputPath, content);
|
|
}
|
|
|
|
/**
|
|
* Creates a package.json for a secondary entry-point.
|
|
* @param dir The directory under which the package.json should be written.
|
|
* @param entryPointPackageName The full package name for the entry point,
|
|
* e.g. '@angular/common/http'.
|
|
*/
|
|
function createEntryPointPackageJson(dir: string, entryPointPackageName: string) {
|
|
const pkgJson = path.join(srcDir, dir, 'package.json');
|
|
const content = amendPackageJson(pkgJson, {name: entryPointPackageName}, true);
|
|
writeFileFromInputPath(pkgJson, content);
|
|
}
|
|
|
|
/**
|
|
* Normalizes the specified path by replacing backslash separators with Posix
|
|
* forward slash separators.
|
|
*/
|
|
function normalizeSeparators(path: string): string {
|
|
return path.replace(/\\/g, '/');
|
|
}
|
|
|
|
/**
|
|
* Rewires metadata to point to the flattened dts file.
|
|
*
|
|
* @param metadataPath the metadata file path
|
|
* @param typingsPath the typings bundle entrypoint
|
|
*/
|
|
function rewireMetadata(metadataPath: string, typingsPath: string): string {
|
|
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
|
|
|
|
let typingsRelativePath =
|
|
normalizeSeparators(path.relative(path.dirname(metadataPath), typingsPath));
|
|
if (!typingsRelativePath.startsWith('..')) {
|
|
typingsRelativePath = `./${typingsRelativePath}`;
|
|
}
|
|
|
|
typingsRelativePath = typingsRelativePath.replace('.d.ts', '');
|
|
|
|
// the regexp here catches all relative paths such as:
|
|
// ./src/core/foo.d.ts and ../src/core/foo.d.ts
|
|
const relativePathRegex = /\.?\.\/[\w\.\-_\/]+/g;
|
|
if (metadata.exports) {
|
|
// Strip re-exports which are now self-references
|
|
metadata.exports =
|
|
metadata.exports.filter((e: {from: string}) => !e.from.match(relativePathRegex));
|
|
}
|
|
return JSON.stringify(metadata).replace(relativePathRegex, typingsRelativePath);
|
|
}
|
|
|
|
/**
|
|
* Strip the named AMD module for compatibility with non-bazel users from typings content
|
|
* @param filePath dts file path
|
|
*/
|
|
function readTypingsAndStripAmdModule(filePath: string): string {
|
|
return fs
|
|
.readFileSync(filePath, 'utf-8')
|
|
// Strip the named AMD module for compatibility with non-bazel users
|
|
.replace(/^\/\/\/ <amd-module name=.*\/>[\r\n]+/gm, '');
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
process.exitCode = main(process.argv.slice(2));
|
|
}
|
|
|
|
export function newArray<T = any>(size: number): T[];
|
|
export function newArray<T>(size: number, value: T): T[];
|
|
export function newArray<T>(size: number, value?: T): T[] {
|
|
const list: T[] = [];
|
|
for (let i = 0; i < size; i++) {
|
|
list.push(value!);
|
|
}
|
|
return list;
|
|
}
|