angular-cli/scripts/build.ts
2018-08-10 18:25:32 -07:00

334 lines
10 KiB
TypeScript

/**
* @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:no-implicit-dependencies
import { JsonObject, logging } from '@angular-devkit/core';
import * as fs from 'fs';
import * as glob from 'glob';
import * as path from 'path';
import * as ts from 'typescript';
import { packages } from '../lib/packages';
const minimatch = require('minimatch');
const tar = require('tar');
const gitIgnore = fs.readFileSync(path.join(__dirname, '../.gitignore'), 'utf-8')
.split('\n')
.map(line => line.replace(/#.*/, ''))
.filter(line => !line.match(/^\s*$/));
function _gitIgnoreMatch(p: string) {
p = path.relative(path.dirname(__dirname), p);
return gitIgnore.some(line => minimatch(p, line));
}
function _mkdirp(p: string) {
// Create parent folder if necessary.
if (!fs.existsSync(path.dirname(p))) {
_mkdirp(path.dirname(p));
}
fs.mkdirSync(p);
}
function _tar(out: string, dir: string) {
return tar.create({
gzip: true,
strict: true,
portable: true,
cwd: dir,
file: out,
sync: true,
}, ['.']);
}
function _copy(from: string, to: string) {
// Create parent folder if necessary.
if (!fs.existsSync(path.dirname(to))) {
_mkdirp(path.dirname(to));
}
from = path.relative(process.cwd(), from);
to = path.relative(process.cwd(), to);
const buffer = fs.readFileSync(from);
fs.writeFileSync(to, buffer);
}
function _recursiveCopy(from: string, to: string, logger: logging.Logger) {
if (!fs.existsSync(from)) {
logger.error(`File "${from}" does not exist.`);
process.exit(4);
}
if (fs.statSync(from).isDirectory()) {
fs.readdirSync(from).forEach(fileName => {
_recursiveCopy(path.join(from, fileName), path.join(to, fileName), logger);
});
} else {
_copy(from, to);
}
}
function _rm(p: string) {
p = path.relative(process.cwd(), p);
fs.unlinkSync(p);
}
function _rimraf(p: string) {
glob.sync(path.join(p, '**/*'), { dot: true, nodir: true })
.forEach(p => fs.unlinkSync(p));
glob.sync(path.join(p, '**/*'), { dot: true })
.sort((a, b) => b.length - a.length)
.forEach(p => fs.rmdirSync(p));
}
function _clean(logger: logging.Logger) {
logger.info('Cleaning...');
logger.info(' Removing dist/...');
_rimraf(path.join(__dirname, '../dist'));
}
function _sortPackages() {
// Order packages in order of dependency.
// We use bubble sort because we need a full topological sort but adding another dependency
// or implementing a full topo sort would be too much work and I'm lazy. We don't anticipate
// any large number of
const sortedPackages = Object.keys(packages);
let swapped = false;
do {
swapped = false;
for (let i = 0; i < sortedPackages.length - 1; i++) {
for (let j = i + 1; j < sortedPackages.length; j++) {
const a = sortedPackages[i];
const b = sortedPackages[j];
if (packages[a].dependencies.indexOf(b) != -1) {
// Swap them.
[sortedPackages[i], sortedPackages[i + 1]]
= [sortedPackages[i + 1], sortedPackages[i]];
swapped = true;
}
}
}
} while (swapped);
return sortedPackages;
}
function _build(logger: logging.Logger) {
logger.info('Building...');
const tsConfigPath = path.relative(process.cwd(), path.join(__dirname, '../tsconfig.json'));
// Load the Compiler Options.
const tsConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
const parsedTsConfig = ts.parseJsonConfigFileContent(tsConfig.config, ts.sys, '.');
// Create the program and emit.
const program = ts.createProgram(parsedTsConfig.fileNames, parsedTsConfig.options);
const result = program.emit();
if (result.emitSkipped) {
logger.error(`TypeScript compiler failed:`);
const diagLogger = logger.createChild('diagnostics');
result.diagnostics.forEach(diagnostic => {
const messageText = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
if (diagnostic.file) {
const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start || 0);
const fileName = diagnostic.file.fileName;
const { line, character } = position;
diagLogger.error(`${fileName} (${line + 1},${character + 1}): ${messageText}`);
} else {
diagLogger.error(messageText);
}
});
process.exit(1);
}
}
export default function(argv: { local?: boolean, snapshot?: boolean }, logger: logging.Logger) {
_clean(logger);
const sortedPackages = _sortPackages();
_build(logger);
logger.info('Moving packages to dist/');
const packageLogger = logger.createChild('packages');
for (const packageName of sortedPackages) {
packageLogger.info(packageName);
const pkg = packages[packageName];
_recursiveCopy(pkg.build, pkg.dist, logger);
_rimraf(pkg.build);
}
logger.info('Copying resources...');
const resourceLogger = logger.createChild('resources');
for (const packageName of sortedPackages) {
resourceLogger.info(packageName);
const pkg = packages[packageName];
const pkgJson = pkg.packageJson;
const files = glob.sync(path.join(pkg.root, '**/*'), { dot: true, nodir: true });
const subSubLogger = resourceLogger.createChild(packageName);
subSubLogger.info(`${files.length} files total...`);
const resources = files
.map((fileName) => path.relative(pkg.root, fileName))
.filter(fileName => {
if (/(?:^|[\/\\])node_modules[\/\\]/.test(fileName)) {
return false;
}
// Schematics template files.
if (pkgJson['schematics'] &&
(fileName.match(/(\/|\\)files(\/|\\)/) || fileName.match(/(\/|\\)\w+-files(\/|\\)/))) {
return true;
}
if (fileName.endsWith('package.json')) {
return true;
}
// Remove Bazel files from NPM.
if (fileName.endsWith('BUILD')) {
return false;
}
// Skip sources.
if (fileName.endsWith('.ts') && !fileName.endsWith('.d.ts')) {
// Verify that it was actually built.
if (!fs.existsSync(path.join(pkg.dist, fileName).replace(/ts$/, 'js'))) {
subSubLogger.error(`\nSource found but compiled file not found: "${fileName}".`);
process.exit(2);
}
// Skip all sources.
return false;
}
// Skip tsconfig only.
if (fileName.endsWith('tsconfig.json')) {
return false;
}
// Skip files from gitignore.
if (_gitIgnoreMatch(fileName)) {
return false;
}
return true;
});
subSubLogger.info(`${resources.length} resources...`);
resources.forEach(fileName => {
_copy(path.join(pkg.root, fileName), path.join(pkg.dist, fileName));
});
}
logger.info('Copying extra resources...');
for (const packageName of sortedPackages) {
const pkg = packages[packageName];
_copy(path.join(__dirname, '../LICENSE'), path.join(pkg.dist, 'LICENSE'));
}
logger.info('Removing spec files...');
const specLogger = logger.createChild('specfiles');
for (const packageName of sortedPackages) {
specLogger.info(packageName);
const pkg = packages[packageName];
const files = glob.sync(path.join(pkg.dist, '**/*_spec?(_large).@(js|d.ts)'));
specLogger.info(` ${files.length} spec files found...`);
files.forEach(fileName => _rm(fileName));
}
logger.info('Building ejs templates...');
const templateLogger = logger.createChild('templates');
const templateCompiler = require('@angular-devkit/core').template;
for (const packageName of sortedPackages) {
templateLogger.info(packageName);
const pkg = packages[packageName];
const files = glob.sync(path.join(pkg.dist, '**/*.ejs'));
templateLogger.info(` ${files.length} ejs files found...`);
files.forEach(fileName => {
const p = path.relative(
path.dirname(__dirname),
path.join(pkg.root, path.relative(pkg.dist, fileName)),
);
const fn = templateCompiler(fs.readFileSync(fileName).toString(), {
module: true,
sourceURL: p,
sourceMap: true,
sourceRoot: path.join(__dirname, '..'),
fileName: fileName.replace(/\.ejs$/, '.js'),
});
_rm(fileName);
fs.writeFileSync(fileName.replace(/\.ejs$/, '.js'), fn.source);
});
}
logger.info('Setting versions...');
const versionLogger = logger.createChild('versions');
for (const packageName of sortedPackages) {
versionLogger.info(packageName);
const pkg = packages[packageName];
const packageJsonPath = path.join(pkg.dist, 'package.json');
const packageJson = pkg.packageJson;
const version = pkg.version;
if (version) {
packageJson['version'] = version;
} else {
versionLogger.error('No version found... Only updating dependencies.');
}
for (const depName of Object.keys(packages)) {
const v = packages[depName].version;
for (const depKey of ['dependencies', 'peerDependencies', 'devDependencies']) {
const obj = packageJson[depKey] as JsonObject | null;
if (obj && obj[depName]) {
if (argv.local) {
obj[depName] = packages[depName].tar;
} else if (argv.snapshot) {
const pkg = packages[depName];
if (!pkg.snapshotRepo) {
versionLogger.error(
`Package ${JSON.stringify(depName)} is not published as a snapshot. `
+ `Fixing to current version ${v}.`,
);
obj[depName] = v;
} else {
obj[depName] = `github:${pkg.snapshotRepo}#${pkg.snapshotHash}`;
}
} else if ((obj[depName] as string).match(/\b0\.0\.0\b/)) {
obj[depName] = (obj[depName] as string).replace(/\b0\.0\.0\b/, v);
}
}
}
}
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
}
logger.info('Tarring all packages...');
const tarLogger = logger.createChild('license');
Object.keys(packages).forEach(pkgName => {
const pkg = packages[pkgName];
tarLogger.info(`${pkgName} => ${pkg.tar}`);
_tar(pkg.tar, pkg.dist);
});
logger.info(`Done.`);
}