Skip to content

Instantly share code, notes, and snippets.

@enten
Last active October 7, 2018 13:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save enten/046463e05b7202a2cfb1f3b231933a83 to your computer and use it in GitHub Desktop.
Save enten/046463e05b7202a2cfb1f3b231933a83 to your computer and use it in GitHub Desktop.
embark-angular-template: angular webpack config builder for embark
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const rimraf = require('rimraf');
const { filter } = require('rxjs/operators');
const { Architect } = require('@angular-devkit/architect');
const {
basename,
dirname,
experimental,
extname,
getSystemPath,
isAbsolute,
join,
logging,
normalize,
relative,
resolve,
terminal,
virtualFs
} = require('@angular-devkit/core');
const { NodeJsSyncHost, resolve: nodeResolve } = require('@angular-devkit/core/node');
class NgConfigBuilder {
constructor(dappPath, embarkPath) {
this.dappPath = dappPath || process.env.DAPP_PATH;
this.embarkPath = embarkPath || process.env.EMBARK_PATH;
this.embarkAliases = require(path.join(this.dappPath, '.embark/embark-aliases.json'));
this.embarkAssets = require(path.join(this.dappPath, '.embark/embark-assets.json'));
this.embarkNodeModules = path.join(this.embarkPath, 'node_modules');
this.embarkJson = require(path.join(this.dappPath, 'embark.json'));
this.buildDir = path.join(this.dappPath, this.embarkJson.buildDir);
// it's important to `embark reset` if a pkg version is specified in
// embark.json and changed/removed later, otherwise pkg resolution may behave
// unexpectedly
try {
this.versions = glob.sync(path.join(this.dappPath, '.embark/versions/*/*'));
} catch (e) {
this.versions = [];
}
this.logger = new logging.IndentLogger('cling');
this.loggingSubscription = initializeLogging(this.logger);
this.workspace = getWorkspace('angular.json');
if (!this.workspace) {
throw new Error('Angular workspace cannot be resolved');
}
this.architect = new Architect(this.workspace);
}
build(targetSpec) {
targetSpec = makeTargetSpecifier(targetSpec);
const building = prepareBuilder(this.architect, targetSpec, this.logger);
const validation = validateConfigCoercion(this, building);
if (!validation.result) {
throw new Error('Cohercion between embark and angular configs failed.\n* ' + validation.errors.join('\n* '));
}
const webpackConfig = buildWebpackConfig(building);
if (targetSpec.configuration === 'production') {
webpackConfig.mode = 'production';
webpackConfig.name = 'production';
} else {
webpackConfig.devtool = 'source-map';
webpackConfig.mode = 'development';
// alternatively:
// development.mode = 'none';
webpackConfig.name = 'development';
}
webpackConfig.resolve.extensions.push('.wasm');
webpackConfig.profile = true;
webpackConfig.stats = 'verbose';
Object.assign(webpackConfig.resolve.alias, this.embarkAliases);
webpackConfig.resolve.modules.push(...this.versions, this.embarkNodeModules);
webpackConfig.resolveLoader.modules.push(this.embarkNodeModules);
return webpackConfig;
}
}
function buildWebpackConfig({ builder, builderConfig, context }) {
const options = builderConfig.options;
const root = context.workspace.root;
const projectRoot = resolve(root, builderConfig.root);
const host = new virtualFs.AliasHost(context.host);
if (options.deleteOutputPath) {
deleteOutputDir(root, normalize(options.outputPath), context.host);
}
if (options.fileReplacements) {
options.fileReplacements = normalizeFileReplacements(options.fileReplacements, host, root);
}
if (options.assets) {
options.assets = normalizeAssetPatterns(options.assets, host, root, projectRoot, builderConfig.sourceRoot);
}
const webpackConfig = builder.buildWebpackConfig(root, projectRoot, host, options);
Object.keys(webpackConfig.optimization.splitChunks.cacheGroups).forEach(key => {
if (webpackConfig.optimization.splitChunks.cacheGroups[key] === undefined) {
delete webpackConfig.optimization.splitChunks.cacheGroups[key];
}
});
return webpackConfig;
}
function deleteOutputDir(root, outputPath, host) {
const resolvedOutputPath = resolve(root, outputPath);
if (resolvedOutputPath === root) {
throw new Error('Output path MUST not be project root directory!');
}
console.log(`rimraf("${resolvedOutputPath}")`);
// rimraf(resolvedOutputPath);
}
function findUp(names, from) {
if (!Array.isArray(names)) {
names = [names];
}
const root = path.parse(from).root;
let currentDir = from;
while (currentDir && currentDir !== root) {
for (const name of names) {
const p = path.join(currentDir, name);
if (fs.existsSync(p)) {
return p;
}
}
currentDir = path.dirname(currentDir);
}
return null;
}
function getWorkspace(configNames, projectPath) {
const configPath = projectFilePath(configNames, projectPath);
if (!configPath) {
return null;
}
const root = normalize(path.dirname(configPath));
const file = normalize(path.basename(configPath));
const workspace = new experimental.workspace.Workspace(root, new NodeJsSyncHost());
workspace.loadWorkspaceFromHost(file).subscribe();
return workspace;
}
function makeTargetSpecifier(targetSpec) {
const [ project, target, configuration ] = targetSpec.split(':');
return {
project,
target,
configuration
};
}
function projectFilePath(configNames, projectPath) {
// Find the configuration, either where specified, in the Angular CLI project
// (if it's in node_modules) or from the current process.
return (projectPath && findUp(configNames, projectPath))
|| findUp(configNames, process.cwd())
|| findUp(configNames, __dirname);
}
function initializeLogging(logger) {
return logger
.pipe(filter(entry => (entry.level != 'debug')))
.subscribe(entry => {
let color = (x) => terminal.dim(terminal.white(x));
let output = process.stdout;
switch (entry.level) {
case 'info':
color = terminal.white;
break;
case 'warn':
color = terminal.yellow;
break;
case 'error':
color = terminal.red;
output = process.stderr;
break;
case 'fatal':
color = (x) => terminal.bold(terminal.red(x));
output = process.stderr;
break;
}
// If we do console.log(message) or process.stdout.write(message + '\n'), the process might
// stop before the whole message is written and the stream is flushed. This happens when
// streams are asynchronous.
//
// NodeJS IO streams are different depending on platform and usage. In POSIX environment,
// for example, they're asynchronous when writing to a pipe, but synchronous when writing
// to a TTY. In windows, it's the other way around. You can verify which is which with
// stream.isTTY and platform, but this is not good enough.
// In the async case, one should wait for the callback before sending more data or
// continuing the process. In our case it would be rather hard to do (but not impossible).
//
// Instead we take the easy way out and simply chunk the message and call the write
// function while the buffer drain itself asynchronously. With a smaller chunk size than
// the buffer, we are mostly certain that it works. In this case, the chunk has been picked
// as half a page size (4096/2 = 2048), minus some bytes for the color formatting.
// On POSIX it seems the buffer is 2 pages (8192), but just to be sure (could be different
// by platform).
//
// For more details, see https://nodejs.org/api/process.html#process_a_note_on_process_i_o
const chunkSize = 2000; // Small chunk.
let message = entry.message;
while (message) {
const chunk = message.slice(0, chunkSize);
message = message.slice(chunkSize);
output.write(color(chunk));
}
output.write('\n');
});
}
function normalizeAssetPatterns(assetPatterns, host, root, projectRoot, maybeSourceRoot) {
// When sourceRoot is not available, we default to ${projectRoot}/src.
const sourceRoot = maybeSourceRoot || join(projectRoot, 'src');
const resolvedSourceRoot = resolve(root, sourceRoot);
if (assetPatterns.length === 0) {
return [];
}
return assetPatterns.map(assetPattern => {
// Normalize string asset patterns to objects.
if (typeof assetPattern === 'string') {
const assetPath = normalize(assetPattern);
const resolvedAssetPath = resolve(root, assetPath);
// Check if the string asset is within sourceRoot.
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(`MissingAssetSourceRootException(${assetPattern})`);
}
// If the path doesn't exist at all, pretend it is a directory.
const isDirectory = fs.existsSync(resolvedAssetPath) ? fs.lstatSync(resolvedAssetPath).isDirectory() : true;
let glob, input, output;
if (isDirectory) {
// Folders get a recursive star glob.
glob = '**/*';
// Input directory is their original path.
input = assetPath;
} else {
// Files are their own glob.
glob = basename(assetPath);
// Input directory is their original dirname.
input = dirname(assetPath);
}
// Output directory for both is the relative path from source root to input.
output = relative(resolvedSourceRoot, resolve(root, input));
// Return the asset pattern in object format.
return { glob, input, output };
} else {
// It's already an AssetPatternObject, no need to convert.
return assetPattern;
}
});
}
function normalizeFileReplacement(fileReplacement, root) {
const currentFormat = fileReplacement;
const maybeOldFormat = fileReplacement;
let replacePath;
let withPath;
if (maybeOldFormat.src && maybeOldFormat.replaceWith) {
replacePath = normalize(maybeOldFormat.src);
withPath = normalize(maybeOldFormat.replaceWith);
} else {
replacePath = normalize(currentFormat.replace);
withPath = normalize(currentFormat.with);
}
// TODO: For 7.x should this only happen if not absolute?
if (root) {
replacePath = join(root, replacePath);
}
if (root) {
withPath = join(root, withPath);
}
return { replace: replacePath, with: withPath };
}
function normalizeFileReplacements(fileReplacements, host, root) {
if (fileReplacements.length === 0) {
return [];
}
return fileReplacements
.map(replacement => normalizeFileReplacement(replacement, root))
.filter(normalized => fs.existsSync(normalized.replace) && fs.existsSync(normalized.with));
}
function prepareBuilder(architect, targetSpec, logger) {
const context = {
logger,
architect,
host: architect._workspace.host,
workspace: architect._workspace
};
const targetMap = architect._workspace.getProjectTargets(targetSpec.project);
architect._targetMapMap.set(targetSpec.project, targetMap);
const builderConfig = architect.getBuilderConfiguration(targetSpec);
const basedir = getSystemPath(context.workspace.root);
const [ pkg, builderName ] = builderConfig.builder.split(':');
const pkgJsonPath = nodeResolve(pkg, { basedir, resolvePackageJson: true, checkLocal: true });
const pkgJson = require(normalize(pkgJsonPath));
const pkgJsonBuildersentry = pkgJson['builders'];
if (!pkgJsonBuildersentry) {
throw new Error('BuilderCannotBeResolvedException: ' + builderConfig.builder);
}
let builderPaths = architect._builderPathsMap.get(builderConfig.builder);
if (!builderPaths) {
let buildersJsonPath = pkgJsonBuildersentry;
if (!isAbsolute(buildersJsonPath)) {
buildersJsonPath = join(dirname(normalize(pkgJsonPath)), pkgJsonBuildersentry);
}
const builderPathsMap = require(buildersJsonPath);
builderPaths = builderPathsMap.builders[builderName];
if (!builderPaths) {
throw new Error('BuilderCannotBeResolvedException: ' + builderConfig.builder);
}
if (!isAbsolute(builderPaths.schema)) {
builderPaths.schema = join(dirname(buildersJsonPath), builderPaths.schema);
}
if (!isAbsolute(builderPaths.class)) {
builderPaths.class = join(dirname(buildersJsonPath), builderPaths.class);
}
architect._builderPathsMap.set(builderConfig.builder, builderPaths);
}
let builderDescription = architect._builderDescriptionMap.get(builderConfig.builder);
if (!builderDescription) {
const builderSchema = require(builderPaths.schema);
builderDescription = {
name: builderConfig.builder,
schema: builderSchema,
description: builderPaths.description
};
architect._builderDescriptionMap.set(builderDescription.name, builderDescription);
}
let builderConstructor = architect._builderConstructorMap.get(builderDescription.name);
if (!builderConstructor) {
builderConstructor = require(getSystemPath(builderPaths.class))['default'];
architect._builderConstructorMap.set(builderDescription.name, builderConstructor);
}
const builder = new builderConstructor(context);
return { builder, builderConfig, context };
}
function validateConfigCoercion(ngConfigBuilder, building) {
const errors = [];
const { buildDir, embarkAssets } = ngConfigBuilder;
const { builderConfig, context } = building;
const options = builderConfig.options;
const root = context.workspace.root;
const projectRoot = resolve(root, builderConfig.root);
let outputPath = options.outputPath;
if (!isAbsolute(outputPath)) {
outputPath = resolve(projectRoot, outputPath);
}
if (normalize(buildDir) !== normalize(outputPath)) {
errors.push(
'Build option outputPath not equals embark option buildDir:\n'
+ ' embark = ' + buildDir + '\n'
+ ' angular = ' + outputPath
);
}
const assetsMap = {};
const assetsList = [];
Object.keys(embarkAssets).forEach(key => {
if (!assetsMap[key]) {
assetsMap[key] = [];
}
embarkAssets[key].forEach(file => {
const filePath = normalize(file.path);
assetsMap[key].push(filePath);
assetsList.push(filePath);
});
});
const checkEmbarkEntryAndNgOption = (entryKey, optionKey) => {
if (!assetsMap[entryKey]) {
errors.push(`Entry ${entryKey} not exists in embark option app`);
} else {
const expectPath = normalize(options[optionKey]);
if (!assetsMap[entryKey].find(x => x === expectPath)) {
errors.push(`Build option ${optionKey} not found in embark option app: ${expectPath}`);
}
}
};
const checkEmbarkEntryAndNgOptionArray = optionKey => {
options[optionKey].forEach(expectPath => {
expectPath = normalize(expectPath);
if (extname(expectPath) && !assetsList.find(x => x === expectPath)) {
errors.push(`Build option ${optionKey} not found in embark option app: ${expectPath}`);
}
});
};
// checkEmbarkEntryAndNgOption('index.html', 'index');
checkEmbarkEntryAndNgOption('main.js', 'main');
if (options.polyfills) {
checkEmbarkEntryAndNgOption('polyfills.js', 'polyfills');
}
checkEmbarkEntryAndNgOptionArray('assets');
checkEmbarkEntryAndNgOptionArray('styles');
checkEmbarkEntryAndNgOptionArray('scripts');
return { result: !errors.length, errors };
}
exports.NgConfigBuilder = NgConfigBuilder;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment