/** * @license * Copyright Google Inc. 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 */ // tslint:disable-next-line:no-implicit-dependencies import { JsonObject } from '@angular-devkit/core'; import { execSync } from 'child_process'; import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; const glob = require('glob'); const distRoot = path.join(__dirname, '../dist'); const { packages: monorepoPackages } = require('../.monorepo.json'); export interface PackageInfo { name: string; root: string; bin: { [name: string]: string}; relative: string; main: string; dist: string; build: string; tar: string; private: boolean; packageJson: JsonObject; dependencies: string[]; snapshot: boolean; snapshotRepo: string; snapshotHash: string; dirty: boolean; hash: string; version: string; } export type PackageMap = { [name: string]: PackageInfo }; const hashCache: {[name: string]: string | null} = {}; function _getHashOf(pkg: PackageInfo): string { if (!(pkg.name in hashCache)) { hashCache[pkg.name] = null; const md5Stream = crypto.createHash('md5'); // Update the stream with all files content. const files: string[] = glob.sync(path.join(pkg.root, '**'), { nodir: true }); files.forEach(filePath => { md5Stream.write(`\0${filePath}\0`); md5Stream.write(fs.readFileSync(filePath)); }); // Update the stream with all versions of upstream dependencies. pkg.dependencies.forEach(depName => { md5Stream.write(`\0${depName}\0${_getHashOf(packages[depName])}\0`); }); md5Stream.end(); hashCache[pkg.name] = (md5Stream.read() as Buffer).toString('hex'); } const value = hashCache[pkg.name]; if (!value) { // Protect against circular dependency. throw new Error('Circular dependency detected between the following packages: ' + Object.keys(hashCache).filter(key => hashCache[key] == null).join(', ')); } return value; } function loadPackageJson(p: string) { const root = require('../package.json'); const pkg = require(p); for (const key of Object.keys(root)) { switch (key) { // Keep the following keys from the package.json of the package itself. case 'bin': case 'description': case 'dependencies': case 'name': case 'main': case 'peerDependencies': case 'optionalDependencies': case 'typings': case 'version': case 'private': case 'workspaces': case 'resolutions': continue; // Remove the following keys from the package.json. case 'devDependencies': case 'scripts': delete pkg[key]; continue; // Merge the following keys with the root package.json. case 'keywords': const a = pkg[key] || []; const b = Object.keys( root[key].concat(a).reduce((acc: {[k: string]: boolean}, curr: string) => { acc[curr] = true; return acc; }, {})); pkg[key] = b; break; // Overwrite engines to a common default. case 'engines': pkg['engines'] = { 'node': '>= 8.9.0', 'npm': '>= 5.5.1', }; break; // Overwrite the package's key with to root one. default: pkg[key] = root[key]; } } return pkg; } function _findAllPackageJson(dir: string, exclude: RegExp): string[] { const result: string[] = []; fs.readdirSync(dir) .forEach(fileName => { const p = path.join(dir, fileName); if (exclude.test(p)) { return; } else if (/[\/\\]node_modules[\/\\]/.test(p)) { return; } else if (fileName == 'package.json') { result.push(p); } else if (fs.statSync(p).isDirectory() && fileName != 'node_modules') { result.push(..._findAllPackageJson(p, exclude)); } }); return result; } const tsConfigPath = path.join(__dirname, '../tsconfig.json'); const tsConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile); const pattern = '^(' + (tsConfig.config.exclude as string[]) .map(ex => path.join(path.dirname(tsConfigPath), ex)) .map(ex => '(' + ex .replace(/[\-\[\]{}()+?./\\^$|]/g, '\\$&') .replace(/(\\\\|\\\/)\*\*/g, '((\/|\\\\).+?)?') .replace(/\*/g, '[^/\\\\]*') + ')') .join('|') + ')($|/|\\\\)'; const excludeRe = new RegExp(pattern); // Find all the package.json that aren't excluded from tsconfig. const packageJsonPaths = _findAllPackageJson(path.join(__dirname, '..'), excludeRe) // Remove the root package.json. .filter(p => p != path.join(__dirname, '../package.json')); let gitShaCache: string; function _getSnapshotHash(_pkg: PackageInfo): string { if (!gitShaCache) { gitShaCache = execSync('git log --format=%h -n1').toString().trim(); } return gitShaCache; } // All the supported packages. Go through the packages directory and create a map of // name => PackageInfo. This map is partial as it lacks some information that requires the // map itself to finish building. export const packages: PackageMap = packageJsonPaths .map(pkgPath => ({ root: path.dirname(pkgPath) })) .reduce((packages: PackageMap, pkg) => { const pkgRoot = pkg.root; const packageJson = loadPackageJson(path.join(pkgRoot, 'package.json')); const name = packageJson['name']; if (!name) { // Only build the entry if there's a package name. return packages; } if (!(name in monorepoPackages)) { throw new Error( `Package ${name} found in ${JSON.stringify(pkg.root)}, not found in .monorepo.json.`, ); } const bin: {[name: string]: string} = {}; Object.keys(packageJson['bin'] || {}).forEach(binName => { let p = path.resolve(pkg.root, packageJson['bin'][binName]); if (!fs.existsSync(p)) { p = p.replace(/\.js$/, '.ts'); } bin[binName] = p; }); packages[name] = { build: path.join(distRoot, pkgRoot.substr(path.dirname(__dirname).length)), dist: path.join(distRoot, name), root: pkgRoot, relative: path.relative(path.dirname(__dirname), pkgRoot), main: path.resolve(pkgRoot, 'src/index.ts'), private: packageJson.private, // yarn doesn't take kindly to @ in tgz filenames // https://github.com/yarnpkg/yarn/issues/6339 tar: path.join(distRoot, name.replace(/\/|@/g, '_') + '.tgz'), bin, name, packageJson, snapshot: !!monorepoPackages[name].snapshotRepo, snapshotRepo: monorepoPackages[name].snapshotRepo, get snapshotHash() { return _getSnapshotHash(this); }, dependencies: [], hash: '', dirty: false, version: monorepoPackages[name] && monorepoPackages[name].version || '0.0.0', }; return packages; }, {}); // Update with dependencies. for (const pkgName of Object.keys(packages)) { const pkg = packages[pkgName]; const pkgJson = require(path.join(pkg.root, 'package.json')); pkg.dependencies = Object.keys(packages).filter(name => { return name in (pkgJson.dependencies || {}) || name in (pkgJson.devDependencies || {}); }); } // Update the hash values of each. for (const pkgName of Object.keys(packages)) { packages[pkgName].hash = _getHashOf(packages[pkgName]); if (!monorepoPackages[pkgName] || packages[pkgName].hash != monorepoPackages[pkgName].hash) { packages[pkgName].dirty = true; } }