@angular-devkit/core
Version:
Angular DevKit - Core Utility Library
231 lines (230 loc) • 9.73 kB
JavaScript
/**
* @license
* Copyright Google LLC 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.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.readJsonWorkspace = readJsonWorkspace;
const jsonc_parser_1 = require("jsonc-parser");
const utils_1 = require("../../json/utils");
const definitions_1 = require("../definitions");
const metadata_1 = require("./metadata");
const utilities_1 = require("./utilities");
const ANGULAR_WORKSPACE_EXTENSIONS = Object.freeze(['cli', 'newProjectRoot', 'schematics']);
const ANGULAR_PROJECT_EXTENSIONS = Object.freeze(['cli', 'schematics', 'projectType', 'i18n']);
async function readJsonWorkspace(path, host, options = {}) {
const raw = await host.readFile(path);
if (raw === undefined) {
throw new Error('Unable to read workspace file.');
}
const ast = (0, jsonc_parser_1.parseTree)(raw, undefined, { allowTrailingComma: true, disallowComments: false });
if (ast?.type !== 'object' || !ast.children) {
throw new Error('Invalid workspace file - expected JSON object.');
}
// Version check
const versionNode = (0, jsonc_parser_1.findNodeAtLocation)(ast, ['version']);
if (!versionNode) {
throw new Error('Unknown format - version specifier not found.');
}
const version = versionNode.value;
if (version !== 1) {
throw new Error(`Invalid format version detected - Expected:[ 1 ] Found: [ ${version} ]`);
}
const context = {
host,
metadata: new metadata_1.JsonWorkspaceMetadata(path, ast, raw),
trackChanges: true,
unprefixedWorkspaceExtensions: new Set([
...ANGULAR_WORKSPACE_EXTENSIONS,
...(options.allowedWorkspaceExtensions ?? []),
]),
unprefixedProjectExtensions: new Set([
...ANGULAR_PROJECT_EXTENSIONS,
...(options.allowedProjectExtensions ?? []),
]),
error(message, _node) {
// TODO: Diagnostic reporting support
throw new Error(message);
},
warn(message, _node) {
// TODO: Diagnostic reporting support
// eslint-disable-next-line no-console
console.warn(message);
},
};
const workspace = parseWorkspace(ast, context);
return workspace;
}
function parseWorkspace(workspaceNode, context) {
const jsonMetadata = context.metadata;
let projects;
let extensions;
if (!context.trackChanges) {
extensions = Object.create(null);
}
// TODO: `getNodeValue` - looks potentially expensive since it walks the whole tree and instantiates the full object structure each time.
// Might be something to look at moving forward to optimize.
const workspaceNodeValue = (0, jsonc_parser_1.getNodeValue)(workspaceNode);
for (const [name, value] of Object.entries(workspaceNodeValue)) {
if (name === '$schema' || name === 'version') {
// skip
}
else if (name === 'projects') {
const nodes = (0, jsonc_parser_1.findNodeAtLocation)(workspaceNode, ['projects']);
if (!(0, utils_1.isJsonObject)(value) || !nodes) {
context.error('Invalid "projects" field found; expected an object.', value);
continue;
}
projects = parseProjectsObject(nodes, context);
}
else {
if (!context.unprefixedWorkspaceExtensions.has(name) && !/^[a-z]{1,3}-.*/.test(name)) {
context.warn(`Workspace extension with invalid name (${name}) found.`, name);
}
if (extensions) {
extensions[name] = value;
}
}
}
let collectionListener;
if (context.trackChanges) {
collectionListener = (name, newValue) => {
jsonMetadata.addChange(['projects', name], newValue, 'project');
};
}
const projectCollection = new definitions_1.ProjectDefinitionCollection(projects, collectionListener);
return {
[metadata_1.JsonWorkspaceSymbol]: jsonMetadata,
projects: projectCollection,
// If not tracking changes the `extensions` variable will contain the parsed
// values. Otherwise the extensions are tracked via a virtual AST object.
extensions: extensions ??
(0, utilities_1.createVirtualAstObject)(workspaceNodeValue, {
exclude: ['$schema', 'version', 'projects'],
listener(path, value) {
jsonMetadata.addChange(path, value);
},
}),
};
}
function parseProjectsObject(projectsNode, context) {
const projects = Object.create(null);
for (const [name, value] of Object.entries((0, jsonc_parser_1.getNodeValue)(projectsNode))) {
const nodes = (0, jsonc_parser_1.findNodeAtLocation)(projectsNode, [name]);
if (!(0, utils_1.isJsonObject)(value) || !nodes) {
context.warn('Skipping invalid project value; expected an object.', value);
continue;
}
projects[name] = parseProject(name, nodes, context);
}
return projects;
}
function parseProject(projectName, projectNode, context) {
const jsonMetadata = context.metadata;
let targets;
let hasTargets = false;
let extensions;
let properties;
if (!context.trackChanges) {
// If not tracking changes, the parser will store the values directly in standard objects
extensions = Object.create(null);
properties = Object.create(null);
}
const projectNodeValue = (0, jsonc_parser_1.getNodeValue)(projectNode);
if (!('root' in projectNodeValue)) {
throw new Error(`Project "${projectName}" is missing a required property "root".`);
}
for (const [name, value] of Object.entries(projectNodeValue)) {
switch (name) {
case 'targets':
case 'architect': {
const nodes = (0, jsonc_parser_1.findNodeAtLocation)(projectNode, [name]);
if (!(0, utils_1.isJsonObject)(value) || !nodes) {
context.error(`Invalid "${name}" field found; expected an object.`, value);
break;
}
hasTargets = true;
targets = parseTargetsObject(projectName, nodes, context);
jsonMetadata.hasLegacyTargetsName = name === 'architect';
break;
}
case 'prefix':
case 'root':
case 'sourceRoot':
if (typeof value !== 'string') {
context.warn(`Project property "${name}" should be a string.`, value);
}
if (properties) {
properties[name] = value;
}
break;
default:
if (!context.unprefixedProjectExtensions.has(name) && !/^[a-z]{1,3}-.*/.test(name)) {
context.warn(`Project '${projectName}' contains extension with invalid name (${name}).`, name);
}
if (extensions) {
extensions[name] = value;
}
break;
}
}
let collectionListener;
if (context.trackChanges) {
collectionListener = (name, newValue, collection) => {
if (hasTargets) {
jsonMetadata.addChange(['projects', projectName, 'targets', name], newValue, 'target');
}
else {
jsonMetadata.addChange(['projects', projectName, 'targets'], collection, 'targetcollection');
}
};
}
const base = {
targets: new definitions_1.TargetDefinitionCollection(targets, collectionListener),
// If not tracking changes the `extensions` variable will contain the parsed
// values. Otherwise the extensions are tracked via a virtual AST object.
extensions: extensions ??
(0, utilities_1.createVirtualAstObject)(projectNodeValue, {
exclude: ['architect', 'prefix', 'root', 'sourceRoot', 'targets'],
listener(path, value) {
jsonMetadata.addChange(['projects', projectName, ...path], value);
},
}),
};
const baseKeys = new Set(Object.keys(base));
const project = properties ??
(0, utilities_1.createVirtualAstObject)(projectNodeValue, {
include: ['prefix', 'root', 'sourceRoot', ...baseKeys],
listener(path, value) {
if (!baseKeys.has(path[0])) {
jsonMetadata.addChange(['projects', projectName, ...path], value);
}
},
});
return Object.assign(project, base);
}
function parseTargetsObject(projectName, targetsNode, context) {
const jsonMetadata = context.metadata;
const targets = Object.create(null);
for (const [name, value] of Object.entries((0, jsonc_parser_1.getNodeValue)(targetsNode))) {
if (!(0, utils_1.isJsonObject)(value)) {
context.warn('Skipping invalid target value; expected an object.', value);
continue;
}
if (context.trackChanges) {
targets[name] = (0, utilities_1.createVirtualAstObject)(value, {
include: ['builder', 'options', 'configurations', 'defaultConfiguration'],
listener(path, value) {
jsonMetadata.addChange(['projects', projectName, 'targets', name, ...path], value);
},
});
}
else {
targets[name] = value;
}
}
return targets;
}
;