1
0
mirror of https://github.com/angular/angular-cli.git synced 2025-05-17 02:54:21 +08:00

build: automate @angular/cli schema.json generation

With this change we automate the generation of `@angular/cli/lib/config/schema.json`. While on paper we could use quicktype for this. Quicktype doesn't handle `patternProperties` and `oneOf` that well.

How does this works?
Relative `$ref` will be resolved and inlined as part of the root schema definitions.

Example
```json
"@schematics/angular:enum": {
    "$ref": "../../../../schematics/angular/enum/schema.json"
},
```

Will be parsed and transformed to
```json
"@schematics/angular:enum": {
  "$ref": "#/definitions/SchematicsAngularEnumSchema"
},
"definitions: {
  "SchematicsAngularEnumSchema": {
    "title": "Angular Enum Options Schema",
    "type": "object",
    "description": "Generates a new, generic enum definition for the given or default project.",
    "properties": {...}
   }
}
```
This commit is contained in:
Alan Agius 2021-03-09 16:54:52 +01:00
parent d254d058f9
commit 4b0223b64e
12 changed files with 782 additions and 2189 deletions

@ -5,6 +5,7 @@
load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
load("//tools:ts_json_schema.bzl", "ts_json_schema")
load("//tools:ng_cli_schema_generator.bzl", "cli_json_schema")
load("//tools:defaults.bzl", "ts_library")
# @external_begin
@ -28,7 +29,7 @@ ts_library(
) + [
# @external_begin
# These files are generated from the JSON schema
"//packages/angular/cli:lib/config/schema.ts",
"//packages/angular/cli:lib/config/workspace-schema.ts",
"//packages/angular/cli:commands/analytics.ts",
"//packages/angular/cli:commands/add.ts",
"//packages/angular/cli:commands/build.ts",
@ -58,8 +59,11 @@ ts_library(
exclude = [
# NB: we need to exclude the nested node_modules that is laid out by yarn workspaces
"node_modules/**",
"cli/lib/config/workspace-schema.json",
],
),
) + [
"//packages/angular/cli:lib/config/schema.json",
],
module_name = "@angular/cli",
# strict_checks = False,
deps = [
@ -84,9 +88,46 @@ ts_library(
],
)
CLI_SCHEMA_DATA = [
"//packages/angular_devkit/build_angular:src/app-shell/schema.json",
"//packages/angular_devkit/build_angular:src/browser/schema.json",
"//packages/angular_devkit/build_angular:src/dev-server/schema.json",
"//packages/angular_devkit/build_angular:src/extract-i18n/schema.json",
"//packages/angular_devkit/build_angular:src/karma/schema.json",
"//packages/angular_devkit/build_angular:src/ng-packagr/schema.json",
"//packages/angular_devkit/build_angular:src/protractor/schema.json",
"//packages/angular_devkit/build_angular:src/server/schema.json",
"//packages/angular_devkit/build_angular:src/tslint/schema.json",
"//packages/schematics/angular:app-shell/schema.json",
"//packages/schematics/angular:application/schema.json",
"//packages/schematics/angular:class/schema.json",
"//packages/schematics/angular:component/schema.json",
"//packages/schematics/angular:directive/schema.json",
"//packages/schematics/angular:enum/schema.json",
"//packages/schematics/angular:guard/schema.json",
"//packages/schematics/angular:interceptor/schema.json",
"//packages/schematics/angular:interface/schema.json",
"//packages/schematics/angular:library/schema.json",
"//packages/schematics/angular:module/schema.json",
"//packages/schematics/angular:ng-new/schema.json",
"//packages/schematics/angular:pipe/schema.json",
"//packages/schematics/angular:resolver/schema.json",
"//packages/schematics/angular:service/schema.json",
"//packages/schematics/angular:service-worker/schema.json",
"//packages/schematics/angular:web-worker/schema.json",
]
cli_json_schema(
name = "cli_config_schema",
src = "lib/config/workspace-schema.json",
out = "lib/config/schema.json",
data = CLI_SCHEMA_DATA,
)
ts_json_schema(
name = "cli_schema",
src = "lib/config/schema.json",
src = "lib/config/workspace-schema.json",
data = CLI_SCHEMA_DATA,
)
ts_json_schema(

@ -9,7 +9,7 @@ import { analytics, tags } from '@angular-devkit/core';
import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools';
import { dirname, join } from 'path';
import { intersects, prerelease, rcompare, satisfies, valid, validRange } from 'semver';
import { PackageManager } from '../lib/config/schema';
import { PackageManager } from '../lib/config/workspace-schema';
import { isPackageNameSafeForAnalytics } from '../models/analytics';
import { Arguments } from '../models/interface';
import { RunSchematicOptions, SchematicCommand } from '../models/schematic-command';

@ -11,7 +11,7 @@ import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as semver from 'semver';
import { PackageManager } from '../lib/config/schema';
import { PackageManager } from '../lib/config/workspace-schema';
import { Command } from '../models/command';
import { Arguments } from '../models/interface';
import { SchematicEngineHost } from '../models/schematic-engine-host';

File diff suppressed because it is too large Load Diff

@ -0,0 +1,569 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "ng-cli://config/schema.json",
"title": "Angular CLI Workspace Configuration",
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"version": {
"$ref": "#/definitions/fileVersion"
},
"cli": {
"$ref": "#/definitions/cliOptions"
},
"schematics": {
"$ref": "#/definitions/schematicOptions"
},
"newProjectRoot": {
"type": "string",
"description": "Path where new projects will be created."
},
"defaultProject": {
"type": "string",
"description": "Default project name used in commands."
},
"projects": {
"type": "object",
"patternProperties": {
"^(?:@[a-zA-Z0-9_-]+\/)?[a-zA-Z0-9_-]+$": {
"$ref": "#/definitions/project"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [
"version"
],
"definitions": {
"cliOptions": {
"type": "object",
"properties": {
"defaultCollection": {
"description": "The default schematics collection to use.",
"type": "string"
},
"packageManager": {
"description": "Specify which package manager tool to use.",
"type": "string",
"enum": [
"npm",
"cnpm",
"yarn",
"pnpm"
]
},
"warnings": {
"description": "Control CLI specific console warnings",
"type": "object",
"properties": {
"versionMismatch": {
"description": "Show a warning when the global version is newer than the local one.",
"type": "boolean"
}
}
},
"analytics": {
"type": [
"boolean",
"string"
],
"description": "Share anonymous usage data with the Angular Team at Google."
},
"analyticsSharing": {
"type": "object",
"properties": {
"tracking": {
"description": "Analytics sharing info tracking ID.",
"type": "string",
"pattern": "^GA-\\d+-\\d+$"
},
"uuid": {
"description": "Analytics sharing info universally unique identifier.",
"type": "string"
}
}
}
},
"additionalProperties": false
},
"schematicOptions": {
"type": "object",
"properties": {
"@schematics/angular:application": {
"$ref": "../../../../schematics/angular/application/schema.json"
},
"@schematics/angular:class": {
"$ref": "../../../../schematics/angular/class/schema.json"
},
"@schematics/angular:component": {
"$ref": "../../../../schematics/angular/component/schema.json"
},
"@schematics/angular:directive": {
"$ref": "../../../../schematics/angular/directive/schema.json"
},
"@schematics/angular:enum": {
"$ref": "../../../../schematics/angular/enum/schema.json"
},
"@schematics/angular:guard": {
"$ref": "../../../../schematics/angular/guard/schema.json"
},
"@schematics/angular:interceptor": {
"$ref": "../../../../schematics/angular/interceptor/schema.json"
},
"@schematics/angular:interface": {
"$ref": "../../../../schematics/angular/interface/schema.json"
},
"@schematics/angular:library": {
"$ref": "../../../../schematics/angular/library/schema.json"
},
"@schematics/angular:pipe": {
"$ref": "../../../../schematics/angular/pipe/schema.json"
},
"@schematics/angular:ng-new": {
"$ref": "../../../../schematics/angular/ng-new/schema.json"
},
"@schematics/angular:resolver": {
"$ref": "../../../../schematics/angular/resolver/schema.json"
},
"@schematics/angular:service": {
"$ref": "../../../../schematics/angular/service/schema.json"
},
"@schematics/angular:web-worker": {
"$ref": "../../../../schematics/angular/web-worker/schema.json"
}
},
"additionalProperties": {
"type": "object"
}
},
"fileVersion": {
"type": "integer",
"description": "File format version",
"minimum": 1
},
"project": {
"type": "object",
"properties": {
"cli": {
"$ref": "#/definitions/cliOptions"
},
"schematics": {
"$ref": "#/definitions/schematicOptions"
},
"prefix": {
"type": "string",
"format": "html-selector",
"description": "The prefix to apply to generated selectors."
},
"root": {
"type": "string",
"description": "Root of the project files."
},
"i18n": {
"$ref": "#/definitions/project/definitions/i18n"
},
"sourceRoot": {
"type": "string",
"description": "The root of the source files, assets and index.html file structure."
},
"projectType": {
"type": "string",
"description": "Project type.",
"enum": [
"application",
"library"
]
},
"architect": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/project/definitions/target"
}
},
"targets": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/project/definitions/target"
}
}
},
"required": [
"root",
"projectType"
],
"anyOf": [
{
"required": [
"architect"
],
"not": {
"required": [
"targets"
]
}
},
{
"required": [
"targets"
],
"not": {
"required": [
"architect"
]
}
},
{
"not": {
"required": [
"targets",
"architect"
]
}
}
],
"additionalProperties": false,
"patternProperties": {
"^[a-z]{1,3}-.*": {}
},
"definitions": {
"i18n": {
"description": "Project i18n options",
"type": "object",
"properties": {
"sourceLocale": {
"oneOf": [
{
"type": "string",
"description": "Specifies the source locale of the application.",
"default": "en-US",
"$comment": "IETF BCP 47 language tag (simplified)",
"pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-[a-zA-Z]{5,8})?(-x(-[a-zA-Z0-9]{1,8})+)?$"
},
{
"type": "object",
"description": "Localization options to use for the source locale",
"properties": {
"code": {
"type": "string",
"description": "Specifies the locale code of the source locale",
"pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-[a-zA-Z]{5,8})?(-x(-[a-zA-Z0-9]{1,8})+)?$"
},
"baseHref": {
"type": "string",
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
}
},
"additionalProperties": false
}
]
},
"locales": {
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-[a-zA-Z]{5,8})?(-x(-[a-zA-Z0-9]{1,8})+)?$": {
"oneOf": [
{
"type": "string",
"description": "Localization file to use for i18n"
},
{
"type": "array",
"description": "Localization files to use for i18n",
"items": {
"type": "string",
"uniqueItems": true
}
},
{
"type": "object",
"description": "Localization options to use for the locale",
"properties": {
"translation": {
"oneOf": [
{
"type": "string",
"description": "Localization file to use for i18n"
},
{
"type": "array",
"description": "Localization files to use for i18n",
"items": {
"type": "string",
"uniqueItems": true
}
}
]
},
"baseHref": {
"type": "string",
"description": "HTML base HREF to use for the locale (defaults to the locale code)"
}
},
"additionalProperties": false
}
]
}
}
}
},
"additionalProperties": false
},
"target": {
"oneOf": [
{
"$comment": "Extendable target with custom builder",
"type": "object",
"properties": {
"builder": {
"type": "string",
"description": "The builder used for this package.",
"not": {
"enum": [
"@angular-devkit/build-angular:app-shell",
"@angular-devkit/build-angular:browser",
"@angular-devkit/build-angular:dev-server",
"@angular-devkit/build-angular:extract-i18n",
"@angular-devkit/build-angular:karma",
"@angular-devkit/build-angular:protractor",
"@angular-devkit/build-angular:server",
"@angular-devkit/build-angular:tslint",
"@angular-devkit/build-angular:ng-packagr"
]
}
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"type": "object"
},
"configurations": {
"type": "object",
"description": "A map of alternative target options.",
"additionalProperties": {
"type": "object"
}
}
},
"required": [
"builder"
]
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:app-shell"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/app-shell/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/app-shell/schema.json"
}
}
}
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:browser"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/browser/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/browser/schema.json"
}
}
}
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:dev-server"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/dev-server/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/dev-server/schema.json"
}
}
}
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:extract-i18n"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/extract-i18n/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/extract-i18n/schema.json"
}
}
}
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:karma"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/karma/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/karma/schema.json"
}
}
}
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:protractor"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/protractor/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/protractor/schema.json"
}
}
}
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:server"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/server/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/server/schema.json"
}
}
}
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:tslint"
},
"defaultConfiguration": {
"type": "string",
"description": "A default named configuration to use when a target configuration is not provided."
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/tslint/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/tslint/schema.json"
}
}
}
},
{
"type": "object",
"properties": {
"builder": {
"const": "@angular-devkit/build-angular:ng-packagr"
},
"options": {
"$ref": "../../../../angular_devkit/build_angular/src/ng-packagr/schema.json"
},
"configurations": {
"type": "object",
"additionalProperties": {
"$ref": "../../../../angular_devkit/build_angular/src/ng-packagr/schema.json"
}
}
}
}
]
}
}
},
"global": {
"type": "object",
"properties": {
"$schema": {
"type": "string",
"format": "uri"
},
"version": {
"$ref": "#/definitions/fileVersion"
},
"cli": {
"$ref": "#/definitions/cliOptions"
},
"schematics": {
"$ref": "#/definitions/schematicOptions"
}
},
"required": [
"version"
]
}
}
}

@ -12,7 +12,7 @@ import { existsSync, mkdtempSync, readFileSync, realpathSync, writeFileSync } fr
import { tmpdir } from 'os';
import { join, resolve } from 'path';
import * as rimraf from 'rimraf';
import { PackageManager } from '../lib/config/schema';
import { PackageManager } from '../lib/config/workspace-schema';
import { colors } from '../utilities/color';
import { NgAddSaveDepedency } from '../utilities/package-metadata';

@ -9,7 +9,7 @@ import { execSync } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
import { satisfies, valid } from 'semver';
import { PackageManager } from '../lib/config/schema';
import { PackageManager } from '../lib/config/workspace-schema';
import { getConfiguredPackageManager } from './config';
function supports(name: string): boolean {

@ -33,7 +33,7 @@ function _rimraf(p: string) {
export default async function(
argv: { },
argv: {},
logger: logging.Logger,
) {
const allJsonFiles = glob.sync('packages/**/*.json', {
@ -77,4 +77,10 @@ export default async function(
_mkdirp(path.dirname(tsPath));
fs.writeFileSync(tsPath, tsContent, 'utf-8');
}
// Angular CLI config schema
const cliJsonSchema = require('../tools/ng_cli_schema_generator');
const inputPath = 'packages/angular/cli/lib/config/workspace-schema.json';
const outputPath = path.join(dist, inputPath.replace('workspace-schema.json', 'schema.json'));
cliJsonSchema.generate(inputPath, outputPath);
}

@ -263,6 +263,11 @@ export default async function(
return false;
}
// This schema is built and copied later on as schema.json.
if (pkg.name === '@angular/cli' && fileName.endsWith('workspace-schema.json')) {
return false;
}
// Remove Bazel files from NPM.
if (fileName === 'BUILD' || fileName === 'BUILD.bazel') {
return false;
@ -303,6 +308,13 @@ export default async function(
for (const packageName of sortedPackages) {
const pkg = packages[packageName];
_copy(path.join(__dirname, '../LICENSE'), path.join(pkg.dist, 'LICENSE'));
if (pkg.name === '@angular/cli') {
_copy(
path.join(__dirname, '../dist-schema/packages/angular/cli/lib/config/schema.json'),
path.join(pkg.dist, 'lib/config/schema.json'),
);
}
}
logger.info('Removing spec files...');

@ -7,6 +7,14 @@ load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
package(default_visibility = ["//visibility:public"])
nodejs_binary(
name = "ng_cli_schema",
data = [
"ng_cli_schema_generator.js",
],
entry_point = "ng_cli_schema_generator.js",
)
nodejs_binary(
name = "quicktype_runner",
data = [

@ -0,0 +1,46 @@
# 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
# @external_begin
def _cli_json_schema_interface_impl(ctx):
args = [
ctx.files.src[0].path,
ctx.outputs.json.path,
]
ctx.actions.run(
inputs = ctx.files.src + ctx.files.data,
executable = ctx.executable._binary,
outputs = [ctx.outputs.json],
arguments = args,
)
return [DefaultInfo()]
cli_json_schema = rule(
_cli_json_schema_interface_impl,
attrs = {
"src": attr.label(
allow_files = [".json"],
mandatory = True,
),
"out": attr.string(
mandatory = True,
),
"data": attr.label_list(
allow_files = [".json"],
mandatory = True,
),
"_binary": attr.label(
default = Label("//tools:ng_cli_schema"),
executable = True,
cfg = "host",
),
},
outputs = {
"json": "%{out}",
},
)
# @external_end

@ -0,0 +1,92 @@
/**
* @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
*/
const { readFileSync, writeFileSync, mkdirSync } = require('fs');
const { resolve, dirname } = require('path');
/**
* Generator the Angular CLI workspace schema file.
*/
function generate(inPath, outPath) {
// While on paper we could use quicktype for this.
// Quicktype doesn't handle `patternProperties` and `oneOf` that well.
const jsonSchema = readFileSync(inPath, 'utf8');
const nestedDefinitions = {};
const schemaParsed = JSON.parse(jsonSchema, (key, value) => {
if (key === '$ref' && typeof value === 'string' && !value.startsWith('#')) {
// Resolve $ref and camelize key
const definitionKey = value
.replace(/(\.json|src)/g, '')
.split(/\\|\/|_|-|\./)
.filter(p => !!p)
.map(s => s.charAt(0).toUpperCase() + s.slice(1))
.join('');
const nestedSchemaPath = resolve(dirname(inPath), value);
const nestedSchema = readFileSync(nestedSchemaPath, 'utf8');
const nestedSchemaJson = JSON.parse(nestedSchema, (key, value) => {
switch (key) {
case '$ref':
if (value.startsWith('#/definitions/')) {
return value.replace('#/definitions/', `#/definitions/${definitionKey}/definitions/`);
} else {
throw new Error(`Error while resolving $ref ${value} in ${nestedSchemaPath}.`);
}
case '$id':
case '$id':
case '$schema':
case 'id':
case 'required':
return undefined;
default:
return value;
}
});
nestedDefinitions[definitionKey] = nestedSchemaJson;
return `#/definitions/${definitionKey}`;
}
return key === ''
? {
...value,
definitions: {
...value.definitions,
...nestedDefinitions,
}
}
: value;
});
const buildWorkspaceDirectory = process.env['BUILD_WORKSPACE_DIRECTORY'] || '.';
outPath = resolve(buildWorkspaceDirectory, outPath);
mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, JSON.stringify(schemaParsed, undefined, 2));
}
if (require.main === module) {
const argv = process.argv.slice(2);
if (argv.length !== 2) {
console.error('Must include 2 arguments.');
process.exit(1);
}
const [inPath, outPath] = argv;
try {
generate(inPath, outPath);
} catch (error) {
console.error('An error happened:');
console.error(err);
process.exit(127);
}
}
exports.generate = generate;