const onSpecCompleted = (format: string) => { if (preserveOutput) { const {tmpdir} = require('os'); const {cp, mkdir, rm, set} = require('shelljs'); const tempRootDir = join(tmpdir(), 'ngcc-spec', format); const outputDir = OUTPUT_PATH; set('-e'); rm('-rf', tempRootDir); mkdir('-p', tempRootDir); cp('-R', join(support.basePath, outputDir), tempRootDir); global.console.log(`Copied '${outputDir}' to '${tempRootDir}'.`); } };
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", // "typing": "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 rolled-up flat ES5 modules fesm5Arg, // List of individual ES2015 modules esm2015Arg, // List of individual ES5 modules esm5Arg, // List of all UMD bundles generated by rollup. bundlesArg, // 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; const fesm2015 = fesm2015Arg.split(',').filter(s => !!s); const fesm5 = fesm5Arg.split(',').filter(s => !!s); const esm2015 = esm2015Arg.split(',').filter(s => !!s); 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) { 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) { // 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'); } /** * 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, 'utf-8')); } /** * Relativize the path where a file is written. * @param file a path containing a re-rooted segment like .esm5 or .es6 * @param suffix the re-rooted directory * @param outDir path where we copy the file, relative to the out */ function writeEsmFile(file: string, suffix: string, outDir: string) { const root = file.substr(0, file.lastIndexOf(suffix + path.sep) + suffix.length + 1); const rel = path.dirname(path.relative(path.join(root, srcDir), file)); if (!rel.startsWith('..')) { copyFile(file, path.join(out, outDir), rel); } } esm2015.forEach(file => writeEsmFile(file, '.es6', 'esm2015')); esm5.forEach(file => writeEsmFile(file, '.esm5', 'esm5')); bundles.forEach(bundle => { copyFile(bundle, out, 'bundles'); }); fesm2015.forEach(file => { copyFile(file, out, 'fesm2015'); }); fesm5.forEach(file => { copyFile(file, out, 'fesm5'); }); const allsrcs = shx.find('-R', binDir); 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/, ''); 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. Object.keys(modulesManifest).forEach(moduleName => { const moduleFiles = modulesManifest[moduleName]; const relative = path.relative(binDir, moduleFiles['index']); moduleFiles['esm5_index'] = path.join(binDir, 'esm5', relative); moduleFiles['esm2015_index'] = path.join(binDir, 'esm2015', relative); copyFileFromInputPath(moduleFiles['metadata']); }); // 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); 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); } const licenseBanner = licenseFile ? fs.readFileSync(licenseFile, 'utf-8') : ''; // Generate extra files for secondary entry-points. Object.keys(modulesManifest).forEach(entryPointPackageName => { const entryPointName = entryPointPackageName.substr(rootPackageName.length + 1); if (!entryPointName) return; createMetadataReexportFile(entryPointName, modulesManifest[entryPointPackageName]['metadata']); 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 = path.relative(path.dirname(from), path.join(srcDir, path.relative(binDir, file))); if (result.startsWith('..')) return result; return `./${result}`; } /** 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 copyFile(file: string, baseDir: string, relative = '.') { const dir = path.join(baseDir, relative); shx.mkdir('-p', dir); shx.cp(file, dir); // 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 (file.indexOf('__') >= 0) { const outputPath = path.join(dir, ...path.basename(file).split('__')); shx.mkdir('-p', path.dirname(outputPath)); shx.mv(path.join(dir, path.basename(file)), 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 */ function amendPackageJson(packageJson: string, parsedPackage: {[key: string]: string}) { const packageName = parsedPackage['name']; const moduleFiles = modulesManifest[packageName]; if (!moduleFiles) { // 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'); 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['fesm5'] = getBundleName(packageName, 'fesm5'); parsedPackage['fesm2015'] = getBundleName(packageName, 'fesm2015'); parsedPackage['esm5'] = srcDirRelative(packageJson, moduleFiles['esm5_index']); parsedPackage['esm2015'] = srcDirRelative(packageJson, moduleFiles['esm2015_index']); parsedPackage['typings'] = srcDirRelative(packageJson, moduleFiles['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. // TODO(iminar): resolve performance issues with the toolchain and point these to esm parsedPackage['module'] = parsedPackage['fesm5']; 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 -> ../../fesm5/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 = Array(nameParts.length - 1).fill('..').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('/'); } /** Creates metadata re-export file for a secondary entry-point. */ function createMetadataReexportFile(entryPointName: string, metadataFile: 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, }) + '\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}); writeFileFromInputPath(pkgJson, content); } }
function main(args: string[]): number { shx.set('-e'); const [out, srcDir, binDir, readmeMd, fesms2015Arg, fesms5Arg, bundlesArg, srcsArg, stampData, licenseFile] = args; const fesms2015 = fesms2015Arg.split(',').filter(s => !!s); const fesms5 = fesms5Arg.split(',').filter(s => !!s); const bundles = bundlesArg.split(',').filter(s => !!s); const srcs = srcsArg.split(',').filter(s => !!s); shx.mkdir('-p', out); let primaryEntryPoint: string|null = null; const secondaryEntryPoints = new Set<string>(); function replaceVersionPlaceholders(filePath: string) { if (stampData) { const version = shx.grep('BUILD_SCM_VERSION', stampData).split(' ')[1].trim(); return shx.sed(/0.0.0-PLACEHOLDER/, version, filePath); } return shx.cat(filePath); } function writeFesm(file: string, baseDir: string) { const parts = path.basename(file).split('__'); const entryPointName = parts.join('/').replace(/\..*/, ''); if (primaryEntryPoint === null || primaryEntryPoint === entryPointName) { primaryEntryPoint = entryPointName; } else { secondaryEntryPoints.add(entryPointName); } 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 moveBundleIndex(f: string) { let ext: string; if (f.endsWith('.d.ts')) ext = '.d.ts'; else if (f.endsWith('.metadata.json')) ext = '.metadata.json'; else throw new Error('Bundle index files should be .d.ts or .metadata.json'); const relative = path.relative(binDir, f); let outputPath: string|undefined = undefined; for (const secondary of secondaryEntryPoints.values()) { if (relative.startsWith(secondary)) { const filename = secondary.split('/').pop(); outputPath = path.join(out, secondary, filename + ext); } } if (!outputPath) { outputPath = path.join(out, primaryEntryPoint + ext); } return outputPath; } if (readmeMd) { shx.cp(readmeMd, path.join(out, 'README.md')); } fesms2015.forEach(fesm2015 => writeFesm(fesm2015, path.join(out, 'esm2015'))); fesms5.forEach(fesm5 => writeFesm(fesm5, path.join(out, 'esm5'))); const bundlesDir = path.join(out, 'bundles'); shx.mkdir('-p', bundlesDir); 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'}) // Strip the named AMD module for compatibility with non-bazel users .replace(/^\/\/\/ <amd-module name=.*\/>\n/, ''); let outputPath: string; if (f.endsWith('.bundle_index.d.ts')) { outputPath = moveBundleIndex(f); } else { outputPath = path.join(out, path.relative(binDir, f)); } shx.mkdir('-p', path.dirname(outputPath)); fs.writeFileSync(outputPath, content); }); for (const src of srcs) { replaceVersionPlaceholders(src).to(path.join(out, path.relative(srcDir, src))); } allsrcs.filter(filter('.bundle_index.metadata.json')).forEach((f: string) => { replaceVersionPlaceholders(f).to(moveBundleIndex(f)); }); const licenseBanner = licenseFile ? fs.readFileSync(licenseFile, {encoding: 'utf-8'}) : ''; for (const secondaryEntryPoint of secondaryEntryPoints.values()) { const baseName = secondaryEntryPoint.split('/').pop(); const dirName = path.join(...secondaryEntryPoint.split('/').slice(0, -1)); fs.writeFileSync(path.join(out, dirName, `${baseName}.metadata.json`), JSON.stringify({ '__symbolic': 'module', 'version': 3, 'metadata': {}, 'exports': [{'from': `./${baseName}/${baseName}`}], 'flatModuleIndexRedirect': true }) + '\n'); fs.writeFileSync( path.join(out, dirName, `${baseName}.d.ts`), // Format carefully to match existing build.sh output licenseBanner + ' ' + ` export * from './${baseName}/${baseName}' `); } return 0; }
function main(args: string[]): number { shx.set('-e'); args = fs.readFileSync(args[0], {encoding: 'utf-8'}).split('\n').map(s => s === '\'\'' ? '' : s); const [out, srcDir, binDir, readmeMd, fesms2015Arg, fesms5Arg, bundlesArg, srcsArg, stampData, licenseFile] = args; const fesms2015 = fesms2015Arg.split(',').filter(s => !!s); const fesms5 = fesms5Arg.split(',').filter(s => !!s); const bundles = bundlesArg.split(',').filter(s => !!s); const srcs = srcsArg.split(',').filter(s => !!s); shx.mkdir('-p', out); let primaryEntryPoint: string|null = null; const secondaryEntryPoints = new Set<string>(); function replaceVersionPlaceholders(filePath: string, content: string) { if (stampData) { const version = shx.grep('BUILD_SCM_VERSION', stampData).split(' ')[1].trim(); // Split the replacement into separate strings so we don't match it while publishing return content.replace( new RegExp( '0.0.0' + '-PLACEHOLDER', 'g'), version); } return content; } /** * 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 indexFile = nameParts[nameParts.length - 1]; parsedPackage['main'] = `${rel}/bundles/${nameParts.join('-')}.umd.js`; parsedPackage['module'] = `${rel}/esm5/${indexFile}.js`; parsedPackage['es2015'] = `${rel}/esm2015/${indexFile}.js`; parsedPackage['typings'] = `./${indexFile}.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(/\..*/, ''); if (primaryEntryPoint === null || primaryEntryPoint === entryPointName) { primaryEntryPoint = entryPointName; } else { secondaryEntryPoints.add(entryPointName); } 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 moveBundleIndex(f: string) { let ext: string; if (f.endsWith('.d.ts')) ext = '.d.ts'; else if (f.endsWith('.metadata.json')) ext = '.metadata.json'; else throw new Error('Bundle index files should be .d.ts or .metadata.json'); const relative = path.relative(binDir, f); let outputPath: string|undefined = undefined; for (const secondary of secondaryEntryPoints.values()) { if (relative.startsWith(secondary)) { const filename = secondary.split('/').pop(); outputPath = path.join(out, secondary, filename + ext); } } if (!outputPath) { outputPath = path.join(out, primaryEntryPoint + ext); } return outputPath; } if (readmeMd) { shx.cp(readmeMd, path.join(out, 'README.md')); } fesms2015.forEach(fesm2015 => writeFesm(fesm2015, path.join(out, 'esm2015'))); fesms5.forEach(fesm5 => writeFesm(fesm5, path.join(out, 'esm5'))); const bundlesDir = path.join(out, 'bundles'); shx.mkdir('-p', bundlesDir); 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'}) // Strip the named AMD module for compatibility with non-bazel users .replace(/^\/\/\/ <amd-module name=.*\/>\n/, ''); let outputPath: string; if (f.endsWith('.bundle_index.d.ts')) { outputPath = moveBundleIndex(f); } else { outputPath = path.join(out, path.relative(binDir, f)); } shx.mkdir('-p', path.dirname(outputPath)); fs.writeFileSync(outputPath, content); }); for (const src of srcs) { let content = fs.readFileSync(src, {encoding: 'utf-8'}); content = replaceVersionPlaceholders(src, content); if (path.basename(src) === 'package.json') { content = amendPackageJson(src, content); } 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), replaceVersionPlaceholders(f, fs.readFileSync(f, {encoding: 'utf-8'}))); }); const licenseBanner = licenseFile ? fs.readFileSync(licenseFile, {encoding: 'utf-8'}) : ''; for (const secondaryEntryPoint of secondaryEntryPoints.values()) { const baseName = secondaryEntryPoint.split('/').pop(); const dirName = path.join(...secondaryEntryPoint.split('/').slice(0, -1)); fs.writeFileSync(path.join(out, dirName, `${baseName}.metadata.json`), JSON.stringify({ '__symbolic': 'module', 'version': 3, 'metadata': {}, 'exports': [{'from': `./${baseName}/${baseName}`}], 'flatModuleIndexRedirect': true }) + '\n'); fs.writeFileSync( path.join(out, dirName, `${baseName}.d.ts`), // Format carefully to match existing build.sh output licenseBanner + ' ' + ` export * from './${baseName}/${baseName}' `); } return 0; }