mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
986 lines
44 KiB
986 lines
44 KiB
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.getLogger = exports.NxScopeHostUsedForWrappedSchematics = exports.NxScopedHostForBuilders = exports.NxScopedHost = void 0;
|
|
exports.createBuilderContext = createBuilderContext;
|
|
exports.scheduleTarget = scheduleTarget;
|
|
exports.arrayBufferToString = arrayBufferToString;
|
|
exports.generate = generate;
|
|
exports.runMigration = runMigration;
|
|
exports.mockSchematicsForTesting = mockSchematicsForTesting;
|
|
exports.wrapAngularDevkitSchematic = wrapAngularDevkitSchematic;
|
|
exports.restoreNxTokensInOptions = restoreNxTokensInOptions;
|
|
const core_1 = require("@angular-devkit/core");
|
|
const node_1 = require("@angular-devkit/core/node");
|
|
const chalk = require("chalk");
|
|
const path_1 = require("path");
|
|
const rxjs_1 = require("rxjs");
|
|
const operators_1 = require("rxjs/operators");
|
|
const tree_1 = require("../generators/tree");
|
|
const json_1 = require("../generators/utils/json");
|
|
const project_configuration_1 = require("../generators/utils/project-configuration");
|
|
const project_graph_1 = require("../project-graph/project-graph");
|
|
const fileutils_1 = require("../utils/fileutils");
|
|
const installation_directory_1 = require("../utils/installation-directory");
|
|
const json_2 = require("../utils/json");
|
|
const logger_1 = require("../utils/logger");
|
|
const package_json_1 = require("../utils/package-json");
|
|
const package_manager_1 = require("../utils/package-manager");
|
|
const angular_json_1 = require("./angular-json");
|
|
const executor_utils_1 = require("../command-line/run/executor-utils");
|
|
const plugins_1 = require("../project-graph/plugins");
|
|
const schema_utils_1 = require("../config/schema-utils");
|
|
const project_configuration_utils_1 = require("../project-graph/utils/project-configuration-utils");
|
|
async function createBuilderContext(builderInfo, context) {
|
|
require('./compat');
|
|
const fsHost = new NxScopedHostForBuilders(context.root);
|
|
// the top level import is not patched because it is imported before the
|
|
// patching happens so we require it here to use the patched version below
|
|
const { workspaces } = require('@angular-devkit/core');
|
|
const { workspace } = await workspaces.readWorkspace('angular.json', workspaces.createWorkspaceHost(fsHost));
|
|
const architectHost = await getWrappedWorkspaceNodeModulesArchitectHost(workspace, context.root, context.projectsConfigurations.projects);
|
|
const registry = new core_1.schema.CoreSchemaRegistry();
|
|
const isAngularBuild = builderInfo.builderName.startsWith('@angular/build:') ||
|
|
['@nx/angular:application', '@nx/angular:unit-test'].includes(builderInfo.builderName);
|
|
if (isAngularBuild) {
|
|
registry.addPostTransform(core_1.schema.transforms.addUndefinedObjectDefaults);
|
|
}
|
|
else {
|
|
registry.addPostTransform(core_1.schema.transforms.addUndefinedDefaults);
|
|
}
|
|
registry.addSmartDefaultProvider('unparsed', () => {
|
|
// This happens when context.scheduleTarget is used to run a target using nx:run-commands
|
|
return [];
|
|
});
|
|
const { Architect } = require('@angular-devkit/architect');
|
|
const architect = new Architect(architectHost, registry);
|
|
const { firstValueFrom } = require('rxjs');
|
|
const toPromise = (obs) => firstValueFrom ? firstValueFrom(obs) : obs.toPromise();
|
|
const validateOptions = (options, builderName) => toPromise(architect['_scheduler'].schedule('..validateOptions', [
|
|
builderName,
|
|
options,
|
|
]).output);
|
|
const getProjectMetadata = (target) => toPromise(architect['_scheduler'].schedule('..getProjectMetadata', target).output);
|
|
const getBuilderNameForTarget = (target) => {
|
|
if (typeof target === 'string') {
|
|
return Promise.resolve(context.projectGraph.nodes[context.projectName].data.targets[target]
|
|
.executor);
|
|
}
|
|
return Promise.resolve(context.projectGraph.nodes[target.project].data.targets[target.target]
|
|
.executor);
|
|
};
|
|
const getTargetOptions = (target) => {
|
|
if (typeof target === 'string') {
|
|
return Promise.resolve({
|
|
...context.projectGraph.nodes[context.projectName].data.targets[target]
|
|
.options,
|
|
});
|
|
}
|
|
return Promise.resolve({
|
|
...context.projectGraph.nodes[target.project].data.targets[target.target]
|
|
.options,
|
|
...context.projectGraph.nodes[target.project].data.targets[target.target]
|
|
.configurations[target.configuration],
|
|
});
|
|
};
|
|
const builderContext = {
|
|
workspaceRoot: context.root,
|
|
target: {
|
|
project: context.projectName,
|
|
target: context.targetName,
|
|
configuration: context.configurationName,
|
|
},
|
|
builder: {
|
|
...builderInfo,
|
|
},
|
|
logger: (0, exports.getLogger)(),
|
|
id: 1,
|
|
currentDirectory: process.cwd(),
|
|
scheduleTarget: (...args) => architect.scheduleTarget(...args),
|
|
scheduleBuilder: (...args) => architect.scheduleBuilder(...args),
|
|
addTeardown(teardown) {
|
|
// No-op as Nx doesn't require an implementation of this function
|
|
return;
|
|
},
|
|
reportProgress(...args) {
|
|
// No-op as Nx doesn't require an implementation of this function
|
|
return;
|
|
},
|
|
reportRunning(...args) {
|
|
// No-op as Nx doesn't require an implementation of this function
|
|
return;
|
|
},
|
|
reportStatus(status) {
|
|
// No-op as Nx doesn't require an implementation of this function
|
|
return;
|
|
},
|
|
getBuilderNameForTarget,
|
|
getProjectMetadata,
|
|
validateOptions,
|
|
getTargetOptions,
|
|
};
|
|
return builderContext;
|
|
}
|
|
async function scheduleTarget(root, opts, verbose) {
|
|
const { Architect } = require('@angular-devkit/architect');
|
|
const logger = (0, exports.getLogger)(verbose);
|
|
const fsHost = new NxScopedHostForBuilders(root);
|
|
const { workspace } = await core_1.workspaces.readWorkspace('angular.json', core_1.workspaces.createWorkspaceHost(fsHost));
|
|
const architectHost = await getWrappedWorkspaceNodeModulesArchitectHost(workspace, root, opts.projects);
|
|
const project = workspace.projects.get(opts.project);
|
|
if (!project) {
|
|
throw new Error(`Cannot find project '${opts.project}' in the workspace`);
|
|
}
|
|
if (!project.targets?.get(opts.target)) {
|
|
throw new Error(`Cannot find target '${opts.target}' for project '${opts.project}'`);
|
|
}
|
|
const builderName = project.targets.get(opts.target).builder;
|
|
if (!builderName) {
|
|
throw new Error(`Cannot find the builder for the target '${opts.target}' of project '${opts.project}'`);
|
|
}
|
|
const isAngularBuild = builderName.startsWith('@angular/build:') ||
|
|
['@nx/angular:application', '@nx/angular:unit-test'].includes(builderName);
|
|
const registry = new core_1.schema.CoreSchemaRegistry();
|
|
if (isAngularBuild) {
|
|
registry.addPostTransform(core_1.schema.transforms.addUndefinedObjectDefaults);
|
|
}
|
|
else {
|
|
registry.addPostTransform(core_1.schema.transforms.addUndefinedDefaults);
|
|
}
|
|
registry.addSmartDefaultProvider('unparsed', () => {
|
|
// This happens when context.scheduleTarget is used to run a target using nx:run-commands
|
|
return [];
|
|
});
|
|
const architect = new Architect(architectHost, registry);
|
|
const run = await architect.scheduleTarget({
|
|
project: opts.project,
|
|
target: opts.target,
|
|
configuration: opts.configuration,
|
|
}, opts.runOptions, { logger });
|
|
let lastOutputError;
|
|
return run.output.pipe((0, operators_1.tap)((output) => (lastOutputError = !output.success ? output.error : undefined), (error) => { }, // do nothing, this could be an intentional error
|
|
() => {
|
|
lastOutputError ? logger.error(lastOutputError) : 0;
|
|
}));
|
|
}
|
|
function createNodeModulesEngineHost(resolvePaths, projects) {
|
|
const NodeModulesEngineHost = require('@angular-devkit/schematics/tools')
|
|
.NodeModulesEngineHost;
|
|
class NxNodeModulesEngineHost extends NodeModulesEngineHost {
|
|
constructor() {
|
|
super(resolvePaths);
|
|
}
|
|
_resolveCollectionPath(name, requester) {
|
|
let collectionFilePath;
|
|
const paths = requester
|
|
? [(0, path_1.dirname)(requester), ...(resolvePaths || [])]
|
|
: resolvePaths || [];
|
|
if (name.endsWith('.json')) {
|
|
collectionFilePath = require.resolve(name, { paths });
|
|
}
|
|
else {
|
|
const { json: { generators, schematics }, path: packageJsonPath, } = (0, plugins_1.readPluginPackageJson)(name, projects, paths);
|
|
if (!schematics && !generators) {
|
|
throw new Error(`The "${name}" package does not support Nx generators or Angular Devkit schematics.`);
|
|
}
|
|
collectionFilePath = require.resolve((0, path_1.join)((0, path_1.dirname)(packageJsonPath), schematics ?? generators));
|
|
}
|
|
return collectionFilePath;
|
|
}
|
|
_transformCollectionDescription(name, desc) {
|
|
desc.schematics ??= desc.generators;
|
|
return super._transformCollectionDescription(name, desc);
|
|
}
|
|
}
|
|
return new NxNodeModulesEngineHost();
|
|
}
|
|
function createWorkflow(fsHost, root, opts, projects) {
|
|
const NodeWorkflow = require('@angular-devkit/schematics/tools').NodeWorkflow;
|
|
const workflow = new NodeWorkflow(fsHost, {
|
|
force: false,
|
|
dryRun: opts.dryRun,
|
|
packageManager: (0, package_manager_1.detectPackageManager)(),
|
|
root: (0, core_1.normalize)(root),
|
|
registry: new core_1.schema.CoreSchemaRegistry(require('@angular-devkit/schematics').formats.standardFormats),
|
|
resolvePaths: [process.cwd(), root],
|
|
engineHostCreator: (options) => createNodeModulesEngineHost(options.resolvePaths, projects),
|
|
});
|
|
workflow.registry.addPostTransform(core_1.schema.transforms.addUndefinedDefaults);
|
|
workflow.engineHost.registerOptionsTransform(require('@angular-devkit/schematics/tools').validateOptionsWithSchema(workflow.registry));
|
|
if (opts.interactive) {
|
|
workflow.registry.usePromptProvider(createPromptProvider());
|
|
}
|
|
return workflow;
|
|
}
|
|
function getCollection(workflow, name) {
|
|
const collection = workflow.engine.createCollection(name);
|
|
if (!collection)
|
|
throw new Error(`Cannot find collection '${name}'`);
|
|
return collection;
|
|
}
|
|
async function createRecorder(host, record, logger) {
|
|
return (event) => {
|
|
let eventPath = event.path.startsWith('/')
|
|
? event.path.slice(1)
|
|
: event.path;
|
|
if (event.kind === 'error') {
|
|
record.error = true;
|
|
logger.warn(`ERROR! ${eventPath} ${event.description == 'alreadyExist'
|
|
? 'already exists'
|
|
: 'does not exist.'}.`);
|
|
}
|
|
else if (event.kind === 'update') {
|
|
record.loggingQueue.push(core_1.tags.oneLine `${chalk.white('UPDATE')} ${eventPath}`);
|
|
}
|
|
else if (event.kind === 'create') {
|
|
record.loggingQueue.push(core_1.tags.oneLine `${chalk.green('CREATE')} ${eventPath}`);
|
|
}
|
|
else if (event.kind === 'delete') {
|
|
record.loggingQueue.push(`${chalk.yellow('DELETE')} ${eventPath}`);
|
|
}
|
|
else if (event.kind === 'rename') {
|
|
record.loggingQueue.push(`${chalk.blue('RENAME')} ${eventPath} => ${event.to}`);
|
|
}
|
|
};
|
|
}
|
|
async function runSchematic(host, root, workflow, logger, opts, schematic, printDryRunMessage = true, recorder = null) {
|
|
const record = { loggingQueue: [], error: false };
|
|
workflow.reporter.subscribe(recorder || (await createRecorder(host, record, logger)));
|
|
try {
|
|
await workflow
|
|
.execute({
|
|
collection: opts.collectionName,
|
|
schematic: opts.generatorName,
|
|
options: opts.generatorOptions,
|
|
debug: false,
|
|
logger,
|
|
})
|
|
.toPromise();
|
|
}
|
|
catch (e) {
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
if (!record.error) {
|
|
record.loggingQueue.forEach((log) => logger.info(log));
|
|
}
|
|
if (opts.dryRun && printDryRunMessage) {
|
|
logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`);
|
|
}
|
|
return { status: 0, loggingQueue: record.loggingQueue };
|
|
}
|
|
class NxScopedHost extends core_1.virtualFs.ScopedHost {
|
|
constructor(root) {
|
|
super(new node_1.NodeJsSyncHost(), (0, core_1.normalize)(root));
|
|
this.root = root;
|
|
}
|
|
read(path) {
|
|
if ((path === 'angular.json' || path === '/angular.json') &&
|
|
(0, angular_json_1.isAngularPluginInstalled)()) {
|
|
return this.readMergedWorkspaceConfiguration().pipe((0, operators_1.map)((r) => stringToArrayBuffer(JSON.stringify((0, angular_json_1.toOldFormat)(r)))));
|
|
}
|
|
else {
|
|
return super.read(path);
|
|
}
|
|
}
|
|
readMergedWorkspaceConfiguration() {
|
|
return (0, rxjs_1.zip)((0, rxjs_1.from)((0, project_graph_1.createProjectGraphAsync)()), this.readExistingAngularJson(), this.readJson('nx.json')).pipe((0, operators_1.concatMap)(([graph, angularJson, nxJson]) => {
|
|
const workspaceConfig = (angularJson || { projects: {} });
|
|
workspaceConfig.cli ??= nxJson.cli;
|
|
workspaceConfig.schematics ??= nxJson.generators;
|
|
const projectJsonReads = [];
|
|
for (let projectName of Object.keys(graph.nodes)) {
|
|
if (!workspaceConfig.projects[projectName]) {
|
|
projectJsonReads.push((0, rxjs_1.zip)((0, rxjs_1.of)(projectName), this.readJson((0, path_1.join)(graph.nodes[projectName].data.root, 'project.json'))));
|
|
}
|
|
}
|
|
return (0, rxjs_1.zip)(...projectJsonReads).pipe((0, operators_1.map)((reads) => {
|
|
reads
|
|
.filter(([, p]) => p !== null)
|
|
.forEach(([projectName, project]) => {
|
|
workspaceConfig.projects[projectName] = {
|
|
...project,
|
|
root: graph.nodes[projectName].data.root,
|
|
};
|
|
});
|
|
return workspaceConfig;
|
|
}));
|
|
}), (0, operators_1.catchError)((err) => {
|
|
console.error('Unable to read angular.json');
|
|
console.error(err);
|
|
process.exit(1);
|
|
}));
|
|
}
|
|
write(path, content) {
|
|
if (path === 'angular.json' || path === '/angular.json') {
|
|
const configV2 = (0, angular_json_1.toNewFormat)((0, json_2.parseJson)(content.toString()));
|
|
const root = this.root;
|
|
return (0, rxjs_1.zip)(this.readMergedWorkspaceConfiguration(), this.readExistingAngularJson()).pipe((0, operators_1.concatMap)((arg) => {
|
|
const existingConfig = arg[0];
|
|
const existingAngularJson = arg[1];
|
|
const projectsInAngularJson = existingAngularJson
|
|
? Object.keys(existingAngularJson.projects)
|
|
: [];
|
|
const projects = configV2.projects;
|
|
const allObservables = [];
|
|
Object.keys(projects).forEach((projectName) => {
|
|
if (!projectsInAngularJson.includes(projectName)) {
|
|
// Restore tokens in options if they were present before
|
|
const previousProject = existingConfig.projects[projectName];
|
|
const newProject = projects[projectName];
|
|
if (previousProject &&
|
|
newProject.targets &&
|
|
previousProject.targets) {
|
|
for (const [targetName, target] of Object.entries(newProject.targets)) {
|
|
const previousTarget = previousProject.targets[targetName];
|
|
if (target.options &&
|
|
previousTarget &&
|
|
previousTarget.options) {
|
|
target.options = restoreNxTokensInOptions(target.options, previousTarget.options, newProject);
|
|
}
|
|
if (target.configurations &&
|
|
previousTarget &&
|
|
previousTarget.configurations) {
|
|
for (const [configName, config] of Object.entries(target.configurations)) {
|
|
if (previousTarget.configurations[configName]) {
|
|
target.configurations[configName] =
|
|
restoreNxTokensInOptions(config, previousTarget.configurations[configName], newProject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
(0, project_configuration_1.updateProjectConfiguration)({
|
|
root,
|
|
exists: () => true,
|
|
write: (path, content) => {
|
|
if (existingConfig.projects[projectName]) {
|
|
const updatedContent = this.mergeProjectConfiguration(existingConfig.projects[projectName], projects[projectName], projectName);
|
|
if (updatedContent) {
|
|
delete updatedContent.root;
|
|
allObservables.push(super.write(path, stringToArrayBuffer(JSON.stringify(updatedContent, null, 2))));
|
|
}
|
|
}
|
|
else {
|
|
allObservables.push(super.write(path, stringToArrayBuffer(content)));
|
|
}
|
|
},
|
|
}, projectName, projects[projectName]);
|
|
}
|
|
});
|
|
return (0, rxjs_1.concat)(...allObservables);
|
|
}));
|
|
}
|
|
else {
|
|
return super.write(path, content);
|
|
}
|
|
}
|
|
isFile(path) {
|
|
if (path === 'angular.json' || path === '/angular.json') {
|
|
return (0, rxjs_1.of)(true);
|
|
}
|
|
else {
|
|
return super.isFile(path);
|
|
}
|
|
}
|
|
exists(path) {
|
|
if (path === 'angular.json' || path === '/angular.json') {
|
|
return (0, rxjs_1.of)(true);
|
|
}
|
|
else {
|
|
return super.exists(path);
|
|
}
|
|
}
|
|
mergeProjectConfiguration(existing, updated, projectName) {
|
|
const res = { ...existing };
|
|
let modified = false;
|
|
function updatePropertyIfDifferent(property) {
|
|
if (typeof res[property] === 'string') {
|
|
if (res[property] !== updated[property]) {
|
|
res[property] = updated[property];
|
|
modified = true;
|
|
}
|
|
}
|
|
else if (JSON.stringify(res[property]) !== JSON.stringify(updated[property])) {
|
|
res[property] = updated[property];
|
|
modified = true;
|
|
}
|
|
}
|
|
if (!res.name || (updated.name && res.name !== updated.name)) {
|
|
res.name ??= updated.name || projectName;
|
|
modified = true;
|
|
}
|
|
updatePropertyIfDifferent('projectType');
|
|
updatePropertyIfDifferent('sourceRoot');
|
|
updatePropertyIfDifferent('prefix');
|
|
updatePropertyIfDifferent('targets');
|
|
updatePropertyIfDifferent('generators');
|
|
updatePropertyIfDifferent('implicitDependencies');
|
|
updatePropertyIfDifferent('tags');
|
|
return modified ? res : null;
|
|
}
|
|
readExistingAngularJson() {
|
|
return this.readJson('angular.json');
|
|
}
|
|
readJson(path) {
|
|
return super
|
|
.exists(path)
|
|
.pipe((0, operators_1.concatMap)((r) => r
|
|
? super
|
|
.read(path)
|
|
.pipe((0, operators_1.map)((r) => (0, json_2.parseJson)(arrayBufferToString(r))))
|
|
: (0, rxjs_1.of)(null)));
|
|
}
|
|
}
|
|
exports.NxScopedHost = NxScopedHost;
|
|
/**
|
|
* Host used by Angular CLI builders. It reads the project configurations from
|
|
* the project graph to access the expanded targets.
|
|
*/
|
|
class NxScopedHostForBuilders extends NxScopedHost {
|
|
readMergedWorkspaceConfiguration() {
|
|
return (0, rxjs_1.zip)((0, rxjs_1.from)((0, project_graph_1.createProjectGraphAsync)()), this.readExistingAngularJson(), this.readJson('nx.json')).pipe((0, operators_1.map)(([graph, angularJson, nxJson]) => {
|
|
const workspaceConfig = (angularJson || { projects: {} });
|
|
workspaceConfig.cli ??= nxJson.cli;
|
|
workspaceConfig.schematics ??= nxJson.generators;
|
|
for (const projectName of Object.keys(graph.nodes)) {
|
|
workspaceConfig.projects[projectName] ??= {
|
|
...graph.nodes[projectName].data,
|
|
};
|
|
}
|
|
return workspaceConfig;
|
|
}), (0, operators_1.catchError)((err) => {
|
|
console.error('Unable to read angular.json');
|
|
console.error(err);
|
|
process.exit(1);
|
|
}));
|
|
}
|
|
}
|
|
exports.NxScopedHostForBuilders = NxScopedHostForBuilders;
|
|
function arrayBufferToString(buffer) {
|
|
const array = new Uint8Array(buffer);
|
|
let result = '';
|
|
const chunkSize = 8 * 1024;
|
|
let i = 0;
|
|
for (i = 0; i < array.length / chunkSize; i++) {
|
|
result += String.fromCharCode.apply(null, array.subarray(i * chunkSize, (i + 1) * chunkSize));
|
|
}
|
|
result += String.fromCharCode.apply(null, array.subarray(i * chunkSize));
|
|
return result;
|
|
}
|
|
/**
|
|
* Host used by Angular CLI schematics. It reads the project configurations from
|
|
* the project configuration files.
|
|
*/
|
|
class NxScopeHostUsedForWrappedSchematics extends NxScopedHost {
|
|
constructor(root, host) {
|
|
super(root);
|
|
this.host = host;
|
|
}
|
|
read(path) {
|
|
if ((path === 'angular.json' || path === '/angular.json') &&
|
|
(0, angular_json_1.isAngularPluginInstalled)()) {
|
|
// Replace the Nx-specific tokens in all target options
|
|
const projects = Object.fromEntries((0, project_configuration_1.getProjects)(this.host));
|
|
for (const [projectName, project] of Object.entries(projects)) {
|
|
if (project.targets) {
|
|
for (const [targetName, target] of Object.entries(project.targets)) {
|
|
if (target.options) {
|
|
target.options = (0, project_configuration_utils_1.resolveNxTokensInOptions)(target.options, { ...project, name: projectName }, `${projectName}:${targetName}`);
|
|
}
|
|
if (target.configurations) {
|
|
for (const [configName, config] of Object.entries(target.configurations)) {
|
|
target.configurations[configName] = (0, project_configuration_utils_1.resolveNxTokensInOptions)(config, { ...project, name: projectName }, `${projectName}:${targetName}:${configName}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const projectJsonConfig = (0, angular_json_1.toOldFormat)({ projects });
|
|
return super.readExistingAngularJson().pipe((0, operators_1.map)((angularJson) => {
|
|
if (angularJson) {
|
|
return stringToArrayBuffer(JSON.stringify({
|
|
version: 1,
|
|
projects: {
|
|
...projectJsonConfig.projects,
|
|
...angularJson.projects,
|
|
},
|
|
}));
|
|
}
|
|
else {
|
|
return stringToArrayBuffer(JSON.stringify(projectJsonConfig));
|
|
}
|
|
}));
|
|
}
|
|
else {
|
|
const match = findMatchingFileChange(this.host, path);
|
|
if (match) {
|
|
return (0, rxjs_1.of)(bufferToArrayBuffer(Buffer.from(match.content)));
|
|
}
|
|
else {
|
|
return super.read(path);
|
|
}
|
|
}
|
|
}
|
|
exists(path) {
|
|
if (this.host.exists(path)) {
|
|
return (0, rxjs_1.of)(true);
|
|
}
|
|
else if (path === 'angular.json' || path === '/angular.json') {
|
|
return (0, rxjs_1.of)(true);
|
|
}
|
|
else {
|
|
return super.exists(path);
|
|
}
|
|
}
|
|
isDirectory(path) {
|
|
if (this.host.exists(path) && !this.host.isFile(path)) {
|
|
return (0, rxjs_1.of)(true);
|
|
}
|
|
else if (path === 'angular.json' || path === '/angular.json') {
|
|
return (0, rxjs_1.of)(false);
|
|
}
|
|
else {
|
|
return super.isDirectory(path);
|
|
}
|
|
}
|
|
isFile(path) {
|
|
if (this.host.isFile(path)) {
|
|
return (0, rxjs_1.of)(true);
|
|
}
|
|
else if (path === 'angular.json' || path === '/angular.json') {
|
|
return (0, rxjs_1.of)(true);
|
|
}
|
|
else {
|
|
return super.isFile(path);
|
|
}
|
|
}
|
|
list(path) {
|
|
const fragments = this.host.children(path).map((child) => (0, core_1.fragment)(child));
|
|
return (0, rxjs_1.of)(fragments);
|
|
}
|
|
}
|
|
exports.NxScopeHostUsedForWrappedSchematics = NxScopeHostUsedForWrappedSchematics;
|
|
function findMatchingFileChange(host, path) {
|
|
const targetPath = (0, core_1.normalize)(path.startsWith('/') ? path.substring(1) : path.toString());
|
|
return host
|
|
.listChanges()
|
|
.find((f) => f.type !== 'DELETE' && (0, core_1.normalize)(f.path) === targetPath);
|
|
}
|
|
async function generate(root, opts, projects, verbose) {
|
|
const logger = (0, exports.getLogger)(verbose);
|
|
const fsHost = new NxScopeHostUsedForWrappedSchematics(root, new tree_1.FsTree(root, verbose, `ng-cli generator: ${opts.collectionName}:${opts.generatorName}`));
|
|
const workflow = createWorkflow(fsHost, root, opts, projects);
|
|
const collection = getCollection(workflow, opts.collectionName);
|
|
const schematic = collection.createSchematic(opts.generatorName, true);
|
|
return (await runSchematic(fsHost, root, workflow, logger, { ...opts, generatorName: schematic.description.name }, schematic)).status;
|
|
}
|
|
function createPromptProvider() {
|
|
return (definitions) => {
|
|
const questions = definitions.map((definition) => {
|
|
const question = {
|
|
name: definition.id,
|
|
message: definition.message,
|
|
};
|
|
if (definition.default) {
|
|
question.initial = definition.default;
|
|
}
|
|
const validator = definition.validator;
|
|
if (validator) {
|
|
question.validate = (input) => validator(input);
|
|
}
|
|
switch (definition.type) {
|
|
case 'string':
|
|
case 'input':
|
|
return { ...question, type: 'input' };
|
|
case 'boolean':
|
|
case 'confirmation':
|
|
case 'confirm':
|
|
return { ...question, type: 'confirm' };
|
|
case 'number':
|
|
case 'numeral':
|
|
return { ...question, type: 'numeral' };
|
|
case 'list':
|
|
return {
|
|
...question,
|
|
type: !!definition.multiselect ? 'multiselect' : 'select',
|
|
choices: definition.items &&
|
|
definition.items.map((item) => {
|
|
if (typeof item == 'string') {
|
|
return item;
|
|
}
|
|
else {
|
|
return {
|
|
message: item.label,
|
|
name: item.value,
|
|
};
|
|
}
|
|
}),
|
|
};
|
|
default:
|
|
return { ...question, type: definition.type };
|
|
}
|
|
});
|
|
return require('enquirer').prompt(questions);
|
|
};
|
|
}
|
|
async function runMigration(root, packageName, migrationName, projects, isVerbose) {
|
|
const logger = (0, exports.getLogger)(isVerbose);
|
|
const fsHost = new NxScopeHostUsedForWrappedSchematics(root, new tree_1.FsTree(root, isVerbose, `ng-cli migration: ${packageName}:${migrationName}`));
|
|
const workflow = createWorkflow(fsHost, root, {}, projects);
|
|
const collection = resolveMigrationsCollection(packageName);
|
|
const record = { loggingQueue: [], error: false };
|
|
workflow.reporter.subscribe(await createRecorder(fsHost, record, logger));
|
|
await workflow
|
|
.execute({
|
|
collection,
|
|
schematic: migrationName,
|
|
options: {},
|
|
debug: false,
|
|
logger: logger,
|
|
})
|
|
.toPromise();
|
|
return {
|
|
loggingQueue: record.loggingQueue,
|
|
madeChanges: record.loggingQueue.length > 0,
|
|
};
|
|
}
|
|
function resolveMigrationsCollection(name) {
|
|
let collectionPath = undefined;
|
|
if (name.startsWith('.') || name.startsWith('/')) {
|
|
name = (0, path_1.resolve)(name);
|
|
}
|
|
if ((0, path_1.extname)(name)) {
|
|
collectionPath = require.resolve(name);
|
|
}
|
|
else {
|
|
const { path: packageJsonPath, packageJson } = (0, package_json_1.readModulePackageJson)(name, (0, installation_directory_1.getNxRequirePaths)(process.cwd()));
|
|
let pkgJsonSchematics = packageJson['nx-migrations'] ?? packageJson['ng-update'];
|
|
if (!pkgJsonSchematics) {
|
|
throw new Error(`Could not find migrations in package: "${name}"`);
|
|
}
|
|
if (typeof pkgJsonSchematics != 'string') {
|
|
pkgJsonSchematics = pkgJsonSchematics.migrations;
|
|
}
|
|
collectionPath = require.resolve(pkgJsonSchematics, {
|
|
paths: [(0, path_1.dirname)(packageJsonPath)],
|
|
});
|
|
}
|
|
try {
|
|
if (collectionPath) {
|
|
(0, fileutils_1.readJsonFile)(collectionPath);
|
|
return collectionPath;
|
|
}
|
|
}
|
|
catch {
|
|
throw new Error(`Invalid migration file in package: "${name}"`);
|
|
}
|
|
throw new Error(`Collection cannot be resolved: "${name}"`);
|
|
}
|
|
let collectionResolutionOverrides = null;
|
|
let mockedSchematics = null;
|
|
/**
|
|
* If you have an Nx Devkit generator invoking the wrapped Angular Devkit schematic,
|
|
* and you don't want the Angular Devkit schematic to run, you can mock it up using this function.
|
|
*
|
|
* Unfortunately, there are some edge cases in the Nx-Angular devkit integration that
|
|
* can be seen in the unit tests context. This function is useful for handling that as well.
|
|
*
|
|
* In this case, you can mock it up.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```typescript
|
|
* mockSchematicsForTesting({
|
|
* 'mycollection:myschematic': (tree, params) => {
|
|
* tree.write("README.md");
|
|
* }
|
|
* });
|
|
*
|
|
* ```
|
|
*/
|
|
function mockSchematicsForTesting(schematics) {
|
|
mockedSchematics = schematics;
|
|
}
|
|
function wrapAngularDevkitSchematic(collectionName, generatorName) {
|
|
// This is idempotent, if it happens to get called
|
|
// multiple times its no big deal. It ensures that some
|
|
// patches are applied to @angular-devkit code which
|
|
// are necessary. For the most part, our wrapped host hits
|
|
// the necessary areas, but for some things it wouldn't make
|
|
// sense for the adapter to be 100% accurate.
|
|
//
|
|
// e.g. Angular warns about tags, but some angular CLI schematics
|
|
// were written with Nx in mind, and may care about tags.
|
|
require('./compat');
|
|
return async (host, generatorOptions) => {
|
|
const graph = await (0, project_graph_1.createProjectGraphAsync)();
|
|
const { projects } = (0, project_graph_1.readProjectsConfigurationFromProjectGraph)(graph);
|
|
if (mockedSchematics &&
|
|
mockedSchematics[`${collectionName}:${generatorName}`]) {
|
|
return await mockedSchematics[`${collectionName}:${generatorName}`](host, generatorOptions);
|
|
}
|
|
const recorder = (event) => {
|
|
let eventPath = event.path.startsWith('/')
|
|
? event.path.slice(1)
|
|
: event.path;
|
|
if (event.kind === 'error') {
|
|
}
|
|
else if (event.kind === 'update') {
|
|
// Apply special handling for the angular.json file, but only when in an Nx workspace
|
|
if (eventPath === 'angular.json' && (0, angular_json_1.isAngularPluginInstalled)()) {
|
|
saveProjectsConfigurationsInWrappedSchematic(host, event.content.toString());
|
|
}
|
|
else {
|
|
host.write(eventPath, toBufferOrString(event.content));
|
|
}
|
|
}
|
|
else if (event.kind === 'create') {
|
|
host.write(eventPath, toBufferOrString(event.content));
|
|
}
|
|
else if (event.kind === 'delete') {
|
|
host.delete(eventPath);
|
|
}
|
|
else if (event.kind === 'rename') {
|
|
host.rename(eventPath, event.to);
|
|
}
|
|
};
|
|
const fsHost = new NxScopeHostUsedForWrappedSchematics(host.root, host);
|
|
const logger = (0, exports.getLogger)(generatorOptions.verbose);
|
|
const options = {
|
|
generatorOptions,
|
|
dryRun: true,
|
|
interactive: false,
|
|
help: false,
|
|
debug: false,
|
|
collectionName,
|
|
generatorName,
|
|
force: false,
|
|
defaults: false,
|
|
quiet: false,
|
|
};
|
|
const workflow = createWorkflow(fsHost, host.root, options, projects);
|
|
// used for testing
|
|
if (collectionResolutionOverrides) {
|
|
const r = workflow.engineHost.resolve;
|
|
workflow.engineHost.resolve = (collection, b, c) => {
|
|
if (collectionResolutionOverrides[collection]) {
|
|
return collectionResolutionOverrides[collection];
|
|
}
|
|
else {
|
|
return r.apply(workflow.engineHost, [collection, b, c]);
|
|
}
|
|
};
|
|
}
|
|
const collection = getCollection(workflow, collectionName);
|
|
const schematic = collection.createSchematic(generatorName, true);
|
|
const res = await runSchematic(fsHost, host.root, workflow, logger, options, schematic, false, recorder);
|
|
if (res.status !== 0) {
|
|
throw new Error(res.loggingQueue.join('\n'));
|
|
}
|
|
const { lastValueFrom } = require('rxjs');
|
|
const toPromise = (obs) => lastValueFrom ? lastValueFrom(obs) : obs.toPromise();
|
|
return async () => {
|
|
// https://github.com/angular/angular-cli/blob/344193f79d880177e421cff85dd3e94338d07420/packages/angular_devkit/schematics/src/workflow/base.ts#L194-L200
|
|
await toPromise(workflow.engine
|
|
.executePostTasks()
|
|
.pipe((0, operators_1.defaultIfEmpty)(undefined), (0, operators_1.last)()));
|
|
};
|
|
};
|
|
}
|
|
let logger;
|
|
const getLogger = (isVerbose = false) => {
|
|
if (!logger) {
|
|
logger = (0, node_1.createConsoleLogger)(isVerbose, process.stdout, process.stderr, {
|
|
warn: (s) => chalk.bold(chalk.yellow(s)),
|
|
error: (s) => {
|
|
if (s.startsWith('NX ')) {
|
|
return `\n${logger_1.NX_ERROR} ${chalk.bold(chalk.red(s.slice(3)))}\n`;
|
|
}
|
|
return chalk.bold(chalk.red(s));
|
|
},
|
|
info: (s) => {
|
|
if (s.startsWith('NX ')) {
|
|
return `\n${logger_1.NX_PREFIX} ${chalk.bold(s.slice(3))}\n`;
|
|
}
|
|
return chalk.white(s);
|
|
},
|
|
});
|
|
}
|
|
return logger;
|
|
};
|
|
exports.getLogger = getLogger;
|
|
function saveProjectsConfigurationsInWrappedSchematic(host, content) {
|
|
const projects = (0, angular_json_1.toNewFormat)((0, json_2.parseJson)(content)).projects;
|
|
const existingProjects = (0, project_configuration_1.getProjects)(host);
|
|
const existingAngularJson = host.exists('angular.json')
|
|
? (0, json_1.readJson)(host, 'angular.json')
|
|
: null;
|
|
const projectsInAngularJson = existingAngularJson
|
|
? Object.keys(existingAngularJson.projects)
|
|
: [];
|
|
const newAngularJson = existingAngularJson || {};
|
|
// Reset projects in order to rebuild them, but leave other properties untouched
|
|
newAngularJson.projects = {};
|
|
Object.keys(projects).forEach((projectName) => {
|
|
if (projectsInAngularJson.includes(projectName)) {
|
|
newAngularJson.projects[projectName] = projects[projectName];
|
|
}
|
|
else {
|
|
if (existingProjects.has(projectName)) {
|
|
if (JSON.stringify(existingProjects.get(projectName)) !==
|
|
JSON.stringify(projects[projectName])) {
|
|
(0, project_configuration_1.updateProjectConfiguration)(host, projectName, projects[projectName]);
|
|
}
|
|
}
|
|
else {
|
|
(0, project_configuration_1.addProjectConfiguration)(host, projectName, projects[projectName]);
|
|
}
|
|
}
|
|
});
|
|
if (Object.keys(newAngularJson.projects).length > 0) {
|
|
host.write('angular.json', JSON.stringify((0, angular_json_1.toOldFormat)(newAngularJson), null, 2));
|
|
}
|
|
}
|
|
async function getWrappedWorkspaceNodeModulesArchitectHost(workspace, root, projects) {
|
|
const { WorkspaceNodeModulesArchitectHost: AngularWorkspaceNodeModulesArchitectHost, } = await Promise.resolve().then(() => require('@angular-devkit/architect/node'));
|
|
class WrappedWorkspaceNodeModulesArchitectHost extends AngularWorkspaceNodeModulesArchitectHost {
|
|
constructor(workspace, root, projects) {
|
|
super(workspace, root);
|
|
this.workspace = workspace;
|
|
this.root = root;
|
|
this.projects = projects;
|
|
}
|
|
async resolveBuilder(builderStr) {
|
|
const [packageName, builderName] = builderStr.split(':');
|
|
const { executorsFilePath, executorConfig } = this.readExecutorsJson(packageName, builderName);
|
|
const builderInfo = this.readExecutor(packageName, builderName);
|
|
return {
|
|
name: builderStr,
|
|
builderName,
|
|
description: executorConfig.description,
|
|
optionSchema: builderInfo.schema,
|
|
import: (0, schema_utils_1.resolveImplementation)(executorConfig.implementation, (0, path_1.dirname)(executorsFilePath), packageName, this.projects),
|
|
};
|
|
}
|
|
readExecutorsJson(nodeModule, builder, extraRequirePaths = []) {
|
|
const { json: packageJson, path: packageJsonPath } = (0, plugins_1.readPluginPackageJson)(nodeModule, this.projects, this.root
|
|
? [this.root, __dirname, ...extraRequirePaths]
|
|
: [__dirname, ...extraRequirePaths]);
|
|
const executorsFile = packageJson.executors ?? packageJson.builders;
|
|
if (!executorsFile) {
|
|
throw new Error(`The "${nodeModule}" package does not support Nx executors or Angular Devkit Builders.`);
|
|
}
|
|
const basePath = (0, path_1.dirname)(packageJsonPath);
|
|
const executorsFilePath = require.resolve((0, path_1.join)(basePath, executorsFile));
|
|
const executorsJson = (0, fileutils_1.readJsonFile)(executorsFilePath);
|
|
const executorConfig = executorsJson.builders?.[builder] ?? executorsJson.executors?.[builder];
|
|
if (!executorConfig) {
|
|
throw new Error(`Cannot find builder '${builder}' in ${executorsFilePath}.`);
|
|
}
|
|
if (typeof executorConfig === 'string') {
|
|
// Angular CLI can have a builder pointing to another package:builder
|
|
const [packageName, executorName] = executorConfig.split(':');
|
|
return this.readExecutorsJson(packageName, executorName, [basePath]);
|
|
}
|
|
return { executorsFilePath, executorConfig, isNgCompat: true };
|
|
}
|
|
readExecutor(nodeModule, executor) {
|
|
try {
|
|
const { executorsFilePath, executorConfig, isNgCompat } = this.readExecutorsJson(nodeModule, executor);
|
|
const executorsDir = (0, path_1.dirname)(executorsFilePath);
|
|
const schemaPath = (0, schema_utils_1.resolveSchema)(executorConfig.schema, executorsDir, nodeModule, this.projects);
|
|
const schema = (0, executor_utils_1.normalizeExecutorSchema)((0, fileutils_1.readJsonFile)(schemaPath));
|
|
const implementationFactory = this.getImplementationFactory(executorConfig.implementation, executorsDir, nodeModule);
|
|
const batchImplementationFactory = executorConfig.batchImplementation
|
|
? this.getImplementationFactory(executorConfig.batchImplementation, executorsDir, nodeModule)
|
|
: null;
|
|
const hasherFactory = executorConfig.hasher
|
|
? this.getImplementationFactory(executorConfig.hasher, executorsDir, nodeModule)
|
|
: null;
|
|
return {
|
|
schema,
|
|
implementationFactory,
|
|
batchImplementationFactory,
|
|
hasherFactory,
|
|
isNgCompat,
|
|
};
|
|
}
|
|
catch (e) {
|
|
throw new Error(`Unable to resolve ${nodeModule}:${executor}.\n${e.message}`);
|
|
}
|
|
}
|
|
getImplementationFactory(implementation, executorsDir, packageName) {
|
|
return (0, schema_utils_1.getImplementationFactory)(implementation, executorsDir, packageName, this.projects);
|
|
}
|
|
}
|
|
return new WrappedWorkspaceNodeModulesArchitectHost(workspace, root, projects);
|
|
}
|
|
/**
|
|
* Restores Nx tokens in options when possible by comparing new and previous
|
|
* options.
|
|
* The function preserves tokens in the following cases:
|
|
* 1. When the resolved previous value matches the new value exactly
|
|
* 2. When the previous value used {workspaceRoot}
|
|
* 3. When the previous value used {projectRoot} and the new value starts with
|
|
* the project root path
|
|
* Those are the only safe cases, for all other cases, the new value is used as-is.
|
|
*/
|
|
function restoreNxTokensInOptions(newOptions, previousOptions, project) {
|
|
if (!newOptions || !previousOptions) {
|
|
return newOptions;
|
|
}
|
|
const result = Array.isArray(newOptions)
|
|
? [...newOptions]
|
|
: { ...newOptions };
|
|
const resolvedPreviousOptions = (0, project_configuration_utils_1.resolveNxTokensInOptions)(previousOptions, project, '');
|
|
for (const key of Object.keys(newOptions)) {
|
|
const newValue = newOptions[key];
|
|
const previousValue = previousOptions[key];
|
|
if (typeof newValue === 'string' && typeof previousValue === 'string') {
|
|
if (resolvedPreviousOptions[key] === newValue) {
|
|
// If the resolved previous value matches the new value, use the previous
|
|
// value (potentially with tokens)
|
|
result[key] = previousValue;
|
|
}
|
|
else if (previousValue.startsWith('{workspaceRoot}/')) {
|
|
// If the previous value started with {workspaceRoot}, prefix the new
|
|
// value with {workspaceRoot}
|
|
result[key] = `{workspaceRoot}/${newValue.replace(/^\//, '')}`;
|
|
}
|
|
else if (previousValue.startsWith('{projectRoot}/') &&
|
|
newValue.startsWith(`${project.root}/`)) {
|
|
// If the previous value started with {projectRoot} and the new value
|
|
// starts with the project root, replace the project root with the
|
|
// {projectRoot} token
|
|
result[key] = newValue.replace(`${project.root}/`, '{projectRoot}/');
|
|
}
|
|
else {
|
|
// Otherwise, use the new value as-is
|
|
result[key] = newValue;
|
|
}
|
|
}
|
|
else if (typeof newValue === 'object' &&
|
|
typeof previousValue === 'object' &&
|
|
newValue &&
|
|
previousValue) {
|
|
result[key] = restoreNxTokensInOptions(newValue, previousValue, project);
|
|
}
|
|
else {
|
|
result[key] = newValue;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function toBufferOrString(content) {
|
|
if (Buffer.isBuffer(content) || typeof content === 'string') {
|
|
return content;
|
|
}
|
|
// it's an ArrayBuffer
|
|
return Buffer.from(content);
|
|
}
|
|
function stringToArrayBuffer(str) {
|
|
return new TextEncoder().encode(str).buffer;
|
|
}
|
|
function bufferToArrayBuffer(buffer) {
|
|
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
|
}
|
|
|