build(bazel): make ng_package auto generate package.json for secondary entry-points (#22806)
PR Close #22806
This commit is contained in:
parent
269c3a1908
commit
b149424b11
|
@ -312,6 +312,8 @@ def _ng_package_impl(ctx):
|
|||
|
||||
args = ctx.actions.args()
|
||||
args.use_param_file("%s", use_always = True)
|
||||
|
||||
# The order of arguments matters here, as they are read in order in packager.ts.
|
||||
args.add(npm_package_directory.path)
|
||||
args.add(ctx.label.package)
|
||||
args.add(primary_entry_point_name(ctx.attr.name, ctx.attr.entry_point))
|
||||
|
|
|
@ -10,17 +10,54 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as shx from 'shelljs';
|
||||
|
||||
function filter(ext: string): (path: string) => boolean {
|
||||
return f => f.endsWith(ext) && !f.endsWith(`.ngfactory${ext}`) && !f.endsWith(`.ngsummary${ext}`);
|
||||
}
|
||||
|
||||
function main(args: string[]): number {
|
||||
// Exit immediately when encountering an error.
|
||||
shx.set('-e');
|
||||
|
||||
args = fs.readFileSync(args[0], {encoding: 'utf-8'}).split('\n').map(s => s === '\'\'' ? '' : s);
|
||||
const
|
||||
[out, srcDir, primaryEntryPoint, secondaryEntryPointsArg, binDir, readmeMd, esm2015Arg,
|
||||
esm5Arg, bundlesArg, srcsArg, licenseFile] = args;
|
||||
// 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];
|
||||
|
||||
// Paramaters are specified in the file one per line. Empty params are represented as two
|
||||
// single-quotes, so turn these into real empty strings..
|
||||
const params =
|
||||
fs.readFileSync(paramFilePath, 'utf-8').split('\n').map(s => s === '\'\'' ? '' : s);
|
||||
|
||||
const [
|
||||
// Output directory for the npm package.
|
||||
out,
|
||||
|
||||
// The package segment of the ng_package rule's label (e.g. 'package/common').
|
||||
srcDir,
|
||||
|
||||
// Path to the JS file for the primaery entry point (e.g. 'packages/common/index.js')
|
||||
primaryEntryPoint,
|
||||
|
||||
// List of secondary entry-points (e.g. ['http', 'http/testing']).
|
||||
secondaryEntryPointsArg,
|
||||
|
||||
// The bazel-bin dir joined with the srcDir (e.g. 'bazel-bin/package.common').
|
||||
// This is the intended output location for package artifacts.
|
||||
binDir,
|
||||
|
||||
// Path to the package's README.md.
|
||||
readmeMd,
|
||||
|
||||
// List of ES2015 files generated by rollup.
|
||||
esm2015Arg,
|
||||
|
||||
// List of flattenned, ES5 files generated by rollup.
|
||||
esm5Arg,
|
||||
|
||||
// List of all UMD bundles generated by rollup.
|
||||
bundlesArg,
|
||||
|
||||
// List of all files in the ng_package rule's srcs.
|
||||
srcsArg,
|
||||
|
||||
// Path to the package's LICENSE.
|
||||
licenseFile] = params;
|
||||
|
||||
const esm2015 = esm2015Arg.split(',').filter(s => !!s);
|
||||
const esm5 = esm5Arg.split(',').filter(s => !!s);
|
||||
const bundles = bundlesArg.split(',').filter(s => !!s);
|
||||
|
@ -29,67 +66,6 @@ function main(args: string[]): number {
|
|||
|
||||
shx.mkdir('-p', out);
|
||||
|
||||
/**
|
||||
* Inserts properties into the package.json file(s) in the package so that
|
||||
* they point to all the right generated artifacts.
|
||||
*
|
||||
* @param filePath file being copied
|
||||
* @param content current file content
|
||||
*/
|
||||
function amendPackageJson(filePath: string, content: string) {
|
||||
const parsedPackage = JSON.parse(content);
|
||||
let nameParts = parsedPackage['name'].split('/');
|
||||
// for scoped packages, we don't care about the scope segment of the path
|
||||
if (nameParts[0].startsWith('@')) nameParts = nameParts.splice(1);
|
||||
let rel = Array(nameParts.length - 1).fill('..').join('/');
|
||||
if (!rel) {
|
||||
rel = '.';
|
||||
}
|
||||
const basename = nameParts[nameParts.length - 1];
|
||||
const indexName = [...nameParts, `${basename}.js`].splice(1).join('/');
|
||||
parsedPackage['main'] = `${rel}/bundles/${nameParts.join('-')}.umd.js`;
|
||||
parsedPackage['module'] = `${rel}/esm5/${indexName}`;
|
||||
parsedPackage['es2015'] = `${rel}/esm2015/${indexName}`;
|
||||
parsedPackage['typings'] = `./${basename}.d.ts`;
|
||||
return JSON.stringify(parsedPackage, null, 2);
|
||||
}
|
||||
|
||||
function writeFesm(file: string, baseDir: string) {
|
||||
const parts = path.basename(file).split('__');
|
||||
const entryPointName = parts.join('/').replace(/\..*/, '');
|
||||
const filename = parts.splice(-1)[0];
|
||||
const dir = path.join(baseDir, ...parts);
|
||||
shx.mkdir('-p', dir);
|
||||
shx.cp(file, dir);
|
||||
shx.mv(path.join(dir, path.basename(file)), path.join(dir, filename));
|
||||
}
|
||||
|
||||
function writeFile(file: string, relative: string, baseDir: string) {
|
||||
const dir = path.join(baseDir, path.dirname(relative));
|
||||
shx.mkdir('-p', dir);
|
||||
shx.cp(file, dir);
|
||||
}
|
||||
|
||||
// Copy these bundle_index outputs from the ng_module rules in the deps
|
||||
// Mapping looks like:
|
||||
// $bin/_core.bundle_index.d.ts
|
||||
// -> $out/core.d.ts
|
||||
// $bin/testing/_testing.bundle_index.d.ts
|
||||
// -> $out/testing/testing.d.ts
|
||||
// $bin/_core.bundle_index.metadata.json
|
||||
// -> $out/core.metadata.json
|
||||
// $bin/testing/_testing.bundle_index.metadata.json
|
||||
// -> $out/testing/testing.metadata.json
|
||||
// JS is a little different, as controlled by the `dir` parameter
|
||||
// $bin/_core.bundle_index.js
|
||||
// -> $out/esm5/core.js
|
||||
// $bin/testing/_testing.bundle_index.js
|
||||
// -> $out/esm5/testing.js
|
||||
function moveBundleIndex(f: string, dir = '.') {
|
||||
const relative = path.relative(binDir, f);
|
||||
return path.join(out, dir, relative.replace(/_(.*)\.bundle_index/, '$1'));
|
||||
}
|
||||
|
||||
if (readmeMd) {
|
||||
shx.cp(readmeMd, path.join(out, 'README.md'));
|
||||
}
|
||||
|
@ -109,8 +85,8 @@ function main(args: string[]): number {
|
|||
bundles.forEach(bundle => { shx.cp(bundle, bundlesDir); });
|
||||
|
||||
const allsrcs = shx.find('-R', binDir);
|
||||
allsrcs.filter(filter('.d.ts')).forEach((f: string) => {
|
||||
const content = fs.readFileSync(f, {encoding: 'utf-8'})
|
||||
allsrcs.filter(hasFileExtension('.d.ts')).forEach((f: string) => {
|
||||
const content = fs.readFileSync(f, 'utf-8')
|
||||
// Strip the named AMD module for compatibility with non-bazel users
|
||||
.replace(/^\/\/\/ <amd-module name=.*\/>\n/, '');
|
||||
let outputPath: string;
|
||||
|
@ -122,51 +98,169 @@ function main(args: string[]): number {
|
|||
shx.mkdir('-p', path.dirname(outputPath));
|
||||
fs.writeFileSync(outputPath, content);
|
||||
});
|
||||
allsrcs.filter(filter('.bundle_index.js')).forEach((f: string) => {
|
||||
const content = fs.readFileSync(f, {encoding: 'utf-8'});
|
||||
allsrcs.filter(hasFileExtension('.bundle_index.js')).forEach((f: string) => {
|
||||
const content = fs.readFileSync(f, 'utf-8');
|
||||
fs.writeFileSync(moveBundleIndex(f, 'esm5'), content);
|
||||
fs.writeFileSync(moveBundleIndex(f, 'esm2015'), content);
|
||||
});
|
||||
|
||||
// Root package name (e.g. '@angular/common'), captures as we iterate through sources below.
|
||||
let rootPackageName = '';
|
||||
const packagesWithExistingPackageJson = new Set<string>();
|
||||
|
||||
// Modify source files as necessary for publishing, including updating the
|
||||
// version placeholders and the paths in any package.json files.
|
||||
for (const src of srcs) {
|
||||
let content = fs.readFileSync(src, {encoding: 'utf-8'});
|
||||
let content = fs.readFileSync(src, 'utf-8');
|
||||
if (path.basename(src) === 'package.json') {
|
||||
content = amendPackageJson(src, content);
|
||||
const packageJson = JSON.parse(content);
|
||||
content = amendPackageJson(packageJson);
|
||||
|
||||
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'];
|
||||
}
|
||||
}
|
||||
const outputPath = path.join(out, path.relative(srcDir, src));
|
||||
shx.mkdir('-p', path.dirname(outputPath));
|
||||
fs.writeFileSync(outputPath, content);
|
||||
}
|
||||
|
||||
allsrcs.filter(filter('.bundle_index.metadata.json')).forEach((f: string) => {
|
||||
fs.writeFileSync(moveBundleIndex(f), fs.readFileSync(f, {encoding: 'utf-8'}));
|
||||
allsrcs.filter(hasFileExtension('.bundle_index.metadata.json')).forEach((f: string) => {
|
||||
fs.writeFileSync(moveBundleIndex(f), fs.readFileSync(f, 'utf-8'));
|
||||
});
|
||||
|
||||
const licenseBanner = licenseFile ? fs.readFileSync(licenseFile, {encoding: 'utf-8'}) : '';
|
||||
const licenseBanner = licenseFile ? fs.readFileSync(licenseFile, 'utf-8') : '';
|
||||
|
||||
// Generate extra files for secondary entry-points.
|
||||
for (const secondaryEntryPoint of secondaryEntryPoints) {
|
||||
const baseName = secondaryEntryPoint.split('/').pop();
|
||||
if (!baseName) throw new Error('secondaryEntryPoint has no slash');
|
||||
const entryPointName = secondaryEntryPoint.split('/').pop();
|
||||
const entryPointPackageName = `${rootPackageName}/${secondaryEntryPoint}`;
|
||||
|
||||
const dirName = path.join(...secondaryEntryPoint.split('/').slice(0, -1));
|
||||
const destDir = path.join(out, dirName);
|
||||
|
||||
fs.writeFileSync(path.join(out, dirName, `${baseName}.metadata.json`), JSON.stringify({
|
||||
'__symbolic': 'module',
|
||||
'version': 3,
|
||||
'metadata': {},
|
||||
'exports': [{'from': `./${baseName}/${baseName}`}],
|
||||
'flatModuleIndexRedirect': true
|
||||
}) + '\n');
|
||||
createMetadataReexportFile(destDir, entryPointName);
|
||||
createTypingsReexportFile(destDir, entryPointName, licenseBanner);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(out, dirName, `${baseName}.d.ts`),
|
||||
// Format carefully to match existing build.sh output
|
||||
licenseBanner + ' ' +
|
||||
`
|
||||
export * from './${baseName}/${baseName}'
|
||||
`);
|
||||
if (!packagesWithExistingPackageJson.has(entryPointPackageName)) {
|
||||
createEntryPointPackageJson(path.join(destDir, entryPointName), entryPointPackageName);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
// Copy these bundle_index outputs from the ng_module rules in the deps
|
||||
// Mapping looks like:
|
||||
// $bin/_core.bundle_index.d.ts
|
||||
// -> $out/core.d.ts
|
||||
// $bin/testing/_testing.bundle_index.d.ts
|
||||
// -> $out/testing/testing.d.ts
|
||||
// $bin/_core.bundle_index.metadata.json
|
||||
// -> $out/core.metadata.json
|
||||
// $bin/testing/_testing.bundle_index.metadata.json
|
||||
// -> $out/testing/testing.metadata.json
|
||||
// JS is a little different, as controlled by the `dir` parameter
|
||||
// $bin/_core.bundle_index.js
|
||||
// -> $out/esm5/core.js
|
||||
// $bin/testing/_testing.bundle_index.js
|
||||
// -> $out/esm5/testing.js
|
||||
function moveBundleIndex(f: string, dir = '.') {
|
||||
const relative = path.relative(binDir, f);
|
||||
return path.join(out, dir, relative.replace(/_(.*)\.bundle_index/, '$1'));
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets a predicate function to filter non-generated files with a specified extension. */
|
||||
function hasFileExtension(ext: string): (path: string) => boolean {
|
||||
return f => f.endsWith(ext) && !f.endsWith(`.ngfactory${ext}`) && !f.endsWith(`.ngsummary${ext}`);
|
||||
}
|
||||
|
||||
function writeFile(file: string, relative: string, baseDir: string) {
|
||||
const dir = path.join(baseDir, path.dirname(relative));
|
||||
shx.mkdir('-p', dir);
|
||||
shx.cp(file, dir);
|
||||
}
|
||||
|
||||
function writeFesm(file: string, baseDir: string) {
|
||||
const parts = path.basename(file).split('__');
|
||||
const entryPointName = parts.join('/').replace(/\..*/, '');
|
||||
const filename = parts.splice(-1)[0];
|
||||
const dir = path.join(baseDir, ...parts);
|
||||
shx.mkdir('-p', dir);
|
||||
shx.cp(file, dir);
|
||||
shx.mv(path.join(dir, path.basename(file)), path.join(dir, filename));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or edits properties into the package.json file(s) in the package so that
|
||||
* they point to all the right generated artifacts.
|
||||
*
|
||||
* @param parsedPackage Parsed package.json content
|
||||
*/
|
||||
function amendPackageJson(parsedPackage: object) {
|
||||
const packageName = parsedPackage['name'];
|
||||
const nameParts = getPackageNameParts(packageName);
|
||||
const relativePathToPackageRoot = getRelativePathToPackageRoot(packageName);
|
||||
const basename = nameParts[nameParts.length - 1];
|
||||
const indexName = [...nameParts, `${basename}.js`].splice(1).join('/');
|
||||
|
||||
parsedPackage['main'] = `${relativePathToPackageRoot}/bundles/${nameParts.join('-')}.umd.js`;
|
||||
parsedPackage['module'] = `${relativePathToPackageRoot}/esm5/${indexName}`;
|
||||
parsedPackage['es2015'] = `${relativePathToPackageRoot}/esm2015/${indexName}`;
|
||||
parsedPackage['typings'] = `./${basename}.d.ts`;
|
||||
return JSON.stringify(parsedPackage, null, 2);
|
||||
}
|
||||
|
||||
/** Gets a package name split into parts, omitting the scope if present. */
|
||||
function getPackageNameParts(fullPackageName: string): string[] {
|
||||
const parts = fullPackageName.split('/');
|
||||
return fullPackageName.startsWith('@') ? parts.splice(1) : parts;
|
||||
}
|
||||
|
||||
/** Gets the relative path to the package root from a given entry-point import path. */
|
||||
function getRelativePathToPackageRoot(entryPointPath: string) {
|
||||
const parts = getPackageNameParts(entryPointPath);
|
||||
const relativePath = Array(parts.length - 1).fill('..').join('/');
|
||||
return relativePath || '.';
|
||||
}
|
||||
|
||||
/** Creates metadata re-export file for a secondary entry-point. */
|
||||
function createMetadataReexportFile(destDir: string, entryPointName: string) {
|
||||
fs.writeFileSync(path.join(destDir, `${entryPointName}.metadata.json`), JSON.stringify({
|
||||
'__symbolic': 'module',
|
||||
'version': 3,
|
||||
'metadata': {},
|
||||
'exports': [{'from': `./${entryPointName}/${entryPointName}`}],
|
||||
'flatModuleIndexRedirect': true
|
||||
}) + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a typings (d.ts) re-export file for a secondary-entry point,
|
||||
* e.g., `export * from './common/common'`
|
||||
*/
|
||||
function createTypingsReexportFile(destDir: string, entryPointName: string, license: string) {
|
||||
// Format carefully to match existing build.sh output:
|
||||
// LICENSE SPACE NEWLINE SPACE EXPORT NEWLINE
|
||||
const content = `${license} \n export * from \'./${entryPointName}/${entryPointName}\n`;
|
||||
fs.writeFileSync(path.join(destDir, `${entryPointName}.d.ts`), content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a package.json for a secondary entry-point.
|
||||
* @param destDir Directory into which the package.json will be written.
|
||||
* @param entryPointPackageName The full package name for the entry point,
|
||||
* e.g. '@angular/common/http'.
|
||||
*/
|
||||
function createEntryPointPackageJson(destDir: string, entryPointPackageName: string) {
|
||||
const content = amendPackageJson({name: entryPointPackageName});
|
||||
fs.writeFileSync(path.join(destDir, 'package.json'), content, 'utf-8');
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
|
|
Loading…
Reference in New Issue