UNPKG

@angular-devkit/core

Version:

Angular DevKit - Core Utility Library

231 lines (230 loc) 9.73 kB
"use strict"; /** * @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; }