diff --git a/packages/bazel/src/ng_package/ng_package.bzl b/packages/bazel/src/ng_package/ng_package.bzl index e01ad06c00..95018efa1a 100644 --- a/packages/bazel/src/ng_package/ng_package.bzl +++ b/packages/bazel/src/ng_package/ng_package.bzl @@ -172,6 +172,7 @@ def _ng_package_impl(ctx): packager_inputs = ( ctx.files.srcs + + ctx.files.data + esm5_sources.to_list() + depset(transitive = [d.typescript.transitive_declarations for d in ctx.attr.deps @@ -186,6 +187,7 @@ def _ng_package_impl(ctx): packager_args.add(npm_package_directory.path) packager_args.add(ctx.label.package) packager_args.add([ctx.bin_dir.path, ctx.label.package], join_with="/") + packager_args.add([ctx.genfiles_dir.path, ctx.label.package], join_with="/") # Marshal the metadata into a JSON string so we can parse the data structure # in the TypeScript program easily. @@ -213,6 +215,9 @@ def _ng_package_impl(ctx): packager_args.add(_flatten_paths(bundles), join_with=",") packager_args.add([s.path for s in ctx.files.srcs], join_with=",") + # TODO: figure out a better way to gather runfiles providers from the transitive closure. + packager_args.add([d.path for d in ctx.files.data], join_with=",") + if ctx.file.license_banner: packager_inputs.append(ctx.file.license_banner) packager_args.add(ctx.file.license_banner.path) @@ -246,6 +251,10 @@ NG_PACKAGE_ATTRS = dict(NPM_PACKAGE_ATTRS, **dict(ROLLUP_ATTRS, **{ esm5_outputs_aspect, sources_aspect, ]), + "data": attr.label_list( + doc = "Additional, non-Angular files to be added to the package, e.g. global CSS assets.", + allow_files = True, + ), "include_devmode_srcs": attr.bool(default = False), "readme_md": attr.label(allow_single_file = FileType([".md"])), "globals": attr.string_dict(default={}), diff --git a/packages/bazel/src/ng_package/packager.ts b/packages/bazel/src/ng_package/packager.ts index 7ef896d669..aedd949632 100644 --- a/packages/bazel/src/ng_package/packager.ts +++ b/packages/bazel/src/ng_package/packager.ts @@ -14,6 +14,9 @@ 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]; @@ -35,6 +38,9 @@ function main(args: string[]): number { // 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": { @@ -67,6 +73,9 @@ function main(args: string[]): number { // List of all files in the ng_package rule's srcs. srcsArg, + // List of all files in the ng_package rule's data. + dataArg, + // Path to the package's LICENSE. licenseFile, ] = params; @@ -77,6 +86,7 @@ function main(args: string[]): number { const esm5 = esm5Arg.split(',').filter(s => !!s); const bundles = bundlesArg.split(',').filter(s => !!s); const srcs = srcsArg.split(',').filter(s => !!s); + const dataFiles: string[] = dataArg.split(',').filter(s => !!s); const modulesManifest = JSON.parse(modulesManifestArg); if (readmeMd) { @@ -84,31 +94,39 @@ function main(args: string[]): number { } /** - * Relativize the path where the file is written. - * @param f a path relative to the srcDir, typically from a file in the srcs[] - * @param c content of the file + * 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 writeSrcFile(f: string, c: string) { - mkDirWriteFile(path.join(out, path.relative(srcDir, f)), c); + function writeFileFromInputPath(inputPath: string, fileContent: string) { + // 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, 'utf-8'); } /** - * Relativize the path where the file is written. - * @param f a path relative to the binDir, typically from a file in the deps[] - * @param c content of the file + * 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 writeBinFile(f: string, c: string) { - const outputPath = path.join(out, path.relative(binDir, f)); - mkDirWriteFile(outputPath, c); - return outputPath; + function copyFileFromInputPath(inputPath: string) { + writeFileFromInputPath(inputPath, fs.readFileSync(inputPath, 'utf-8')); } - /** - * Copy the file, relativizing the path. - * @param f a path relative to the binDir, typically from a file in the deps[] - */ - function copyBinFile(f: string) { writeBinFile(f, fs.readFileSync(f, 'utf-8')); } - /** * Relativize the path where a file is written. * @param file a path containing a re-rooted segment like .esm5 or .es6 @@ -135,9 +153,13 @@ function main(args: string[]): number { const content = fs.readFileSync(f, 'utf-8') // Strip the named AMD module for compatibility with non-bazel users .replace(/^\/\/\/ \n/, ''); - const outputPath = writeBinFile(f, content); + writeFileFromInputPath(f, content); }); + // 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. @@ -149,11 +171,11 @@ function main(args: string[]): number { moduleFiles['esm5_index'] = path.join(binDir, 'esm5', relative); moduleFiles['esm2015_index'] = path.join(binDir, 'esm2015', relative); - writeBinFile(moduleFiles['esm5_index'], indexContent); - writeBinFile(moduleFiles['esm2015_index'], indexContent); + writeFileFromInputPath(moduleFiles['esm5_index'], indexContent); + writeFileFromInputPath(moduleFiles['esm2015_index'], indexContent); - copyBinFile(moduleFiles['typings']); - copyBinFile(moduleFiles['metadata']); + copyFileFromInputPath(moduleFiles['typings']); + copyFileFromInputPath(moduleFiles['metadata']); }); // Root package name (e.g. '@angular/common'), captures as we iterate through sources below. @@ -161,6 +183,13 @@ function main(args: string[]): number { const packagesWithExistingPackageJson = new Set(); 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') { @@ -177,7 +206,7 @@ function main(args: string[]): number { rootPackageName = packageJson['name']; } } - writeSrcFile(src, content); + writeFileFromInputPath(src, content); } const licenseBanner = licenseFile ? fs.readFileSync(licenseFile, 'utf-8') : ''; @@ -196,7 +225,7 @@ function main(args: string[]): number { } }); - return 0; + return errorHasOccured ? 1 : 0; /** * Convert a binDir-relative path to srcDir-relative @@ -216,11 +245,6 @@ function main(args: string[]): number { !f.endsWith(`.ngsummary${ext}`); } - function mkDirWriteFile(p: string, content: string) { - shx.mkdir('-p', path.dirname(p)); - fs.writeFileSync(p, content, 'utf-8'); - } - function copyFile(file: string, baseDir: string, relative = '.') { const dir = path.join(baseDir, relative); shx.mkdir('-p', dir); @@ -240,6 +264,7 @@ function main(args: string[]): number { * 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 */ function amendPackageJson(packageJson: string, parsedPackage: {[key: string]: string}) { @@ -298,13 +323,13 @@ function main(args: string[]): number { /** Creates metadata re-export file for a secondary entry-point. */ function createMetadataReexportFile(entryPointName: string, metadataFile: string) { const inputPath = path.join(srcDir, `${entryPointName}.metadata.json`); - writeSrcFile(inputPath, JSON.stringify({ + writeFileFromInputPath(inputPath, JSON.stringify({ '__symbolic': 'module', 'version': 3, 'metadata': {}, 'exports': [{'from': `${srcDirRelative(inputPath, metadataFile.replace(/.metadata.json$/, ''))}`}], - 'flatModuleIndexRedirect': true + 'flatModuleIndexRedirect': true, }) + '\n'); } @@ -317,18 +342,19 @@ function main(args: string[]): number { const content = `${license} export * from '${srcDirRelative(inputPath, typingsFile.replace(/\.d\.tsx?$/, ''))}'; `; - writeSrcFile(inputPath, content); + 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}); - writeSrcFile(pkgJson, content); + writeFileFromInputPath(pkgJson, content); } } diff --git a/packages/bazel/test/ng_package/example/BUILD.bazel b/packages/bazel/test/ng_package/example/BUILD.bazel index cba62be187..fefc424bba 100644 --- a/packages/bazel/test/ng_package/example/BUILD.bazel +++ b/packages/bazel/test/ng_package/example/BUILD.bazel @@ -15,9 +15,31 @@ ng_package( "package.json", "some-file.txt", ], + data = [ + ":arbitrary_bin_file", + ":arbitrary_genfiles_file", + ":extra-styles.css", + ], entry_point = "packages/bazel/test/ng_package/example/index.js", deps = [ ":example", "//packages/bazel/test/ng_package/example/secondary", ], ) + +# Use a genrule to create a file in bazel-genfiles to ensure that the genfiles output of +# a rule can be passed through to the `data` of ng_package. +genrule( + name = "arbitrary_genfiles_file", + outs = ["arbitrary_genfiles.txt"], + cmd = "echo Hello > $@", +) + +# Use a genrule to create a file in bazel-bin to ensure that the bin output of +# a rule can be passed through to the `data` of ng_package. +genrule( + name = "arbitrary_bin_file", + outs = ["arbitrary_bin.txt"], + cmd = "echo World > $@", + output_to_bindir = True, +) diff --git a/packages/bazel/test/ng_package/example/extra-styles.css b/packages/bazel/test/ng_package/example/extra-styles.css new file mode 100644 index 0000000000..0b5e2be6f7 --- /dev/null +++ b/packages/bazel/test/ng_package/example/extra-styles.css @@ -0,0 +1,3 @@ +.special { + color: goldenrod; +} diff --git a/packages/bazel/test/ng_package/example_package.golden b/packages/bazel/test/ng_package/example_package.golden index 8a9becff8d..3cbb38d515 100644 --- a/packages/bazel/test/ng_package/example_package.golden +++ b/packages/bazel/test/ng_package/example_package.golden @@ -1,3 +1,5 @@ +arbitrary_bin.txt +arbitrary_genfiles.txt bundles bundles/example-secondary.umd.js bundles/example-secondary.umd.js.map @@ -43,6 +45,7 @@ esm5 esm5/secondary/secondarymodule.ngsummary.js example_public_index.d.ts example_public_index.metadata.json +extra-styles.css fesm2015 fesm2015/example.js fesm2015/example.js.map @@ -65,6 +68,16 @@ secondary secondary.d.ts secondary.metadata.json some-file.txt +--- arbitrary_bin.txt --- + +World + + +--- arbitrary_genfiles.txt --- + +Hello + + --- bundles/example-secondary.umd.js --- (function (global, factory) { @@ -661,6 +674,13 @@ export * from './index'; {"__symbolic":"module","version":4,"metadata":{"MyModule":{"__symbolic":"class","decorators":[{"__symbolic":"call","expression":{"__symbolic":"reference","module":"@angular/core","name":"NgModule","line":11,"character":1},"arguments":[{}]}],"members":{}}},"origins":{"MyModule":"./mymodule"},"importAs":"example"} +--- extra-styles.css --- + +.special { + color: goldenrod; +} + + --- fesm2015/example.js --- import { NgModule } from '@angular/core';