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 = ctx.actions.args()
|
||||||
args.use_param_file("%s", use_always = True)
|
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(npm_package_directory.path)
|
||||||
args.add(ctx.label.package)
|
args.add(ctx.label.package)
|
||||||
args.add(primary_entry_point_name(ctx.attr.name, ctx.attr.entry_point))
|
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 path from 'path';
|
||||||
import * as shx from 'shelljs';
|
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 {
|
function main(args: string[]): number {
|
||||||
|
// Exit immediately when encountering an error.
|
||||||
shx.set('-e');
|
shx.set('-e');
|
||||||
|
|
||||||
args = fs.readFileSync(args[0], {encoding: 'utf-8'}).split('\n').map(s => s === '\'\'' ? '' : s);
|
// This utility expects all of its arguments to be specified in a params file generated by
|
||||||
const
|
// bazel (see https://docs.bazel.build/versions/master/skylark/lib/Args.html#use_param_file).
|
||||||
[out, srcDir, primaryEntryPoint, secondaryEntryPointsArg, binDir, readmeMd, esm2015Arg,
|
const paramFilePath = args[0];
|
||||||
esm5Arg, bundlesArg, srcsArg, licenseFile] = args;
|
|
||||||
|
// 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 esm2015 = esm2015Arg.split(',').filter(s => !!s);
|
||||||
const esm5 = esm5Arg.split(',').filter(s => !!s);
|
const esm5 = esm5Arg.split(',').filter(s => !!s);
|
||||||
const bundles = bundlesArg.split(',').filter(s => !!s);
|
const bundles = bundlesArg.split(',').filter(s => !!s);
|
||||||
|
@ -29,67 +66,6 @@ function main(args: string[]): number {
|
||||||
|
|
||||||
shx.mkdir('-p', out);
|
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) {
|
if (readmeMd) {
|
||||||
shx.cp(readmeMd, path.join(out, 'README.md'));
|
shx.cp(readmeMd, path.join(out, 'README.md'));
|
||||||
}
|
}
|
||||||
|
@ -109,8 +85,8 @@ function main(args: string[]): number {
|
||||||
bundles.forEach(bundle => { shx.cp(bundle, bundlesDir); });
|
bundles.forEach(bundle => { shx.cp(bundle, bundlesDir); });
|
||||||
|
|
||||||
const allsrcs = shx.find('-R', binDir);
|
const allsrcs = shx.find('-R', binDir);
|
||||||
allsrcs.filter(filter('.d.ts')).forEach((f: string) => {
|
allsrcs.filter(hasFileExtension('.d.ts')).forEach((f: string) => {
|
||||||
const content = fs.readFileSync(f, {encoding: 'utf-8'})
|
const content = fs.readFileSync(f, 'utf-8')
|
||||||
// Strip the named AMD module for compatibility with non-bazel users
|
// Strip the named AMD module for compatibility with non-bazel users
|
||||||
.replace(/^\/\/\/ <amd-module name=.*\/>\n/, '');
|
.replace(/^\/\/\/ <amd-module name=.*\/>\n/, '');
|
||||||
let outputPath: string;
|
let outputPath: string;
|
||||||
|
@ -122,51 +98,169 @@ function main(args: string[]): number {
|
||||||
shx.mkdir('-p', path.dirname(outputPath));
|
shx.mkdir('-p', path.dirname(outputPath));
|
||||||
fs.writeFileSync(outputPath, content);
|
fs.writeFileSync(outputPath, content);
|
||||||
});
|
});
|
||||||
allsrcs.filter(filter('.bundle_index.js')).forEach((f: string) => {
|
allsrcs.filter(hasFileExtension('.bundle_index.js')).forEach((f: string) => {
|
||||||
const content = fs.readFileSync(f, {encoding: 'utf-8'});
|
const content = fs.readFileSync(f, 'utf-8');
|
||||||
fs.writeFileSync(moveBundleIndex(f, 'esm5'), content);
|
fs.writeFileSync(moveBundleIndex(f, 'esm5'), content);
|
||||||
fs.writeFileSync(moveBundleIndex(f, 'esm2015'), 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) {
|
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') {
|
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));
|
const outputPath = path.join(out, path.relative(srcDir, src));
|
||||||
shx.mkdir('-p', path.dirname(outputPath));
|
shx.mkdir('-p', path.dirname(outputPath));
|
||||||
fs.writeFileSync(outputPath, content);
|
fs.writeFileSync(outputPath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
allsrcs.filter(filter('.bundle_index.metadata.json')).forEach((f: string) => {
|
allsrcs.filter(hasFileExtension('.bundle_index.metadata.json')).forEach((f: string) => {
|
||||||
fs.writeFileSync(moveBundleIndex(f), fs.readFileSync(f, {encoding: 'utf-8'}));
|
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) {
|
for (const secondaryEntryPoint of secondaryEntryPoints) {
|
||||||
const baseName = secondaryEntryPoint.split('/').pop();
|
const entryPointName = secondaryEntryPoint.split('/').pop();
|
||||||
if (!baseName) throw new Error('secondaryEntryPoint has no slash');
|
const entryPointPackageName = `${rootPackageName}/${secondaryEntryPoint}`;
|
||||||
|
|
||||||
const dirName = path.join(...secondaryEntryPoint.split('/').slice(0, -1));
|
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({
|
createMetadataReexportFile(destDir, entryPointName);
|
||||||
'__symbolic': 'module',
|
createTypingsReexportFile(destDir, entryPointName, licenseBanner);
|
||||||
'version': 3,
|
|
||||||
'metadata': {},
|
|
||||||
'exports': [{'from': `./${baseName}/${baseName}`}],
|
|
||||||
'flatModuleIndexRedirect': true
|
|
||||||
}) + '\n');
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
if (!packagesWithExistingPackageJson.has(entryPointPackageName)) {
|
||||||
path.join(out, dirName, `${baseName}.d.ts`),
|
createEntryPointPackageJson(path.join(destDir, entryPointName), entryPointPackageName);
|
||||||
// Format carefully to match existing build.sh output
|
}
|
||||||
licenseBanner + ' ' +
|
|
||||||
`
|
|
||||||
export * from './${baseName}/${baseName}'
|
|
||||||
`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
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) {
|
if (require.main === module) {
|
||||||
|
|
Loading…
Reference in New Issue