diff --git a/angular.json b/angular.json index ab1dac87d1..d118730eee 100644 --- a/angular.json +++ b/angular.json @@ -619,6 +619,38 @@ } } } + }, + "vscode-json-schema": { + "root": "libs/vscode/json-schema", + "sourceRoot": "libs/vscode/json-schema/src", + "projectType": "library", + "architect": { + "lint": { + "builder": "@nrwl/linter:eslint", + "options": { + "lintFilePatterns": ["libs/vscode/json-schema/**/*.ts"] + } + }, + "test": { + "builder": "@nrwl/jest:jest", + "outputs": ["coverage/libs/vscode/json-schema"], + "options": { + "jestConfig": "libs/vscode/json-schema/jest.config.js", + "passWithNoTests": true + } + }, + "build": { + "builder": "@nrwl/node:package", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/vscode/json-schema", + "tsConfig": "libs/vscode/json-schema/tsconfig.lib.json", + "packageJson": "libs/vscode/json-schema/package.json", + "main": "libs/vscode/json-schema/src/index.ts", + "assets": ["libs/vscode/json-schema/*.md"] + } + } + } } } } diff --git a/apps/vscode/src/main.ts b/apps/vscode/src/main.ts index 85a7fec4d4..65d504c7e5 100644 --- a/apps/vscode/src/main.ts +++ b/apps/vscode/src/main.ts @@ -51,6 +51,8 @@ import { } from '@nx-console/vscode/nx-workspace'; import { environment } from './environments/environment'; +import { WorkspaceJsonSchema } from '@nx-console/vscode/json-schema'; + let runTargetTreeView: TreeView; let nxProjectTreeView: TreeView; let nxCommandsTreeView: TreeView; @@ -126,6 +128,7 @@ export function activate(c: ExtensionContext) { // registers itself as a CodeLensProvider and watches config to dispose/re-register new WorkspaceCodeLensProvider(context); + new WorkspaceJsonSchema(context); getTelemetry().extensionActivated((Date.now() - startTime) / 1000); } catch (e) { diff --git a/apps/vscode/src/package.json b/apps/vscode/src/package.json index c47bb291a3..ea03945cbd 100644 --- a/apps/vscode/src/package.json +++ b/apps/vscode/src/package.json @@ -639,6 +639,16 @@ "when": "isNxWorkspace || isAngularWorkspace" } ] - } + }, + "jsonValidation": [ + { + "fileMatch": "workspace.json", + "url": "./workspace-schema.json" + }, + { + "fileMatch": "angular.json", + "url": "./workspace-schema.json" + } + ] } } diff --git a/jest.config.js b/jest.config.js index dff135dc12..9fca12fefa 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,5 +13,6 @@ module.exports = { '/libs/vscode/nx-run-target-view', '/libs/vscode/nx-commands-view', '/libs/vscode/webview', + '/libs/vscode/json-schema', ], }; diff --git a/libs/server/src/index.ts b/libs/server/src/index.ts index afeb69e1eb..58b82f3faa 100644 --- a/libs/server/src/index.ts +++ b/libs/server/src/index.ts @@ -13,5 +13,6 @@ export { readAndCacheJsonFile, clearJsonCache, toLegacyWorkspaceFormat, + listOfUnnestedNpmPackages, } from './lib/utils/utils'; export { watchFile } from './lib/utils/watch-file'; diff --git a/libs/server/src/lib/utils/read-schematic-collections.ts b/libs/server/src/lib/utils/read-schematic-collections.ts index 6f931549a3..83b71004bc 100644 --- a/libs/server/src/lib/utils/read-schematic-collections.ts +++ b/libs/server/src/lib/utils/read-schematic-collections.ts @@ -207,7 +207,8 @@ export async function readSchematicOptions( nodeModulesDir ); const collectionJson = readAndCacheJsonFile( - collectionPackageJson.json.schematics || collectionPackageJson.json.generators, + collectionPackageJson.json.schematics || + collectionPackageJson.json.generators, dirname(collectionPackageJson.path) ); const schematicSchema = readAndCacheJsonFile( diff --git a/libs/vscode/json-schema/.eslintrc.json b/libs/vscode/json-schema/.eslintrc.json new file mode 100644 index 0000000000..6aca353b76 --- /dev/null +++ b/libs/vscode/json-schema/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "parserOptions": { + "project": ["libs/vscode/json-schema/tsconfig.*?.json"] + }, + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/vscode/json-schema/README.md b/libs/vscode/json-schema/README.md new file mode 100644 index 0000000000..a4c92ccd9f --- /dev/null +++ b/libs/vscode/json-schema/README.md @@ -0,0 +1,7 @@ +# vscode-json-schema + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test vscode-json-schema` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/vscode/json-schema/jest.config.js b/libs/vscode/json-schema/jest.config.js new file mode 100644 index 0000000000..6a5cba2983 --- /dev/null +++ b/libs/vscode/json-schema/jest.config.js @@ -0,0 +1,14 @@ +module.exports = { + displayName: 'vscode-json-schema', + preset: '../../../jest.preset.js', + globals: { + 'ts-jest': { + tsConfig: '/tsconfig.spec.json', + }, + }, + transform: { + '^.+\\.[tj]sx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/vscode/json-schema', +}; diff --git a/libs/vscode/json-schema/package.json b/libs/vscode/json-schema/package.json new file mode 100644 index 0000000000..803d8c9849 --- /dev/null +++ b/libs/vscode/json-schema/package.json @@ -0,0 +1,4 @@ +{ + "name": "@nx-console/vscode/json-schema", + "version": "0.0.1" +} diff --git a/libs/vscode/json-schema/src/index.ts b/libs/vscode/json-schema/src/index.ts new file mode 100644 index 0000000000..30031a8013 --- /dev/null +++ b/libs/vscode/json-schema/src/index.ts @@ -0,0 +1 @@ +export * from './lib/workspace-json-schema'; diff --git a/libs/vscode/json-schema/src/lib/get-all-executors.ts b/libs/vscode/json-schema/src/lib/get-all-executors.ts new file mode 100644 index 0000000000..b88646a69c --- /dev/null +++ b/libs/vscode/json-schema/src/lib/get-all-executors.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { clearJsonCache, readAndCacheJsonFile } from '@nx-console/server'; +import { dirname, join } from 'path'; + +export interface ExecutorInfo { + name: string; + path: string; +} + +export function getAllExecutors( + workspaceJsonPath: string, + clearPackageJsonCache: boolean +): ExecutorInfo[] { + return readExecutorCollectionsFromNodeModules( + workspaceJsonPath, + clearPackageJsonCache + ); +} + +function readExecutorCollectionsFromNodeModules( + workspaceJsonPath: string, + clearPackageJsonCache: boolean +): ExecutorInfo[] { + const basedir = join(workspaceJsonPath, '..'); + const nodeModulesDir = join(basedir, 'node_modules'); + + if (clearPackageJsonCache) { + clearJsonCache('package.json', basedir); + } + + const packages: { [packageName: string]: string } = + readAndCacheJsonFile('package.json', basedir).json.devDependencies || {}; + const executorCollections = Object.keys(packages).filter((p) => { + try { + const packageJson = readAndCacheJsonFile( + join(p, 'package.json'), + nodeModulesDir + ).json; + // TODO: to add support for schematics, we can change this to include schematics/generators + return !!(packageJson.builders || packageJson.executors); + } catch (e) { + if ( + e.message && + (e.message.indexOf('no such file') > -1 || + e.message.indexOf('not a directory') > -1) + ) { + return false; + } else { + throw e; + } + } + }); + + return executorCollections + .map((c) => readCollections(nodeModulesDir, c)) + .flat() + .filter((c): c is ExecutorInfo => Boolean(c)); +} + +function readCollections( + basedir: string, + collectionName: string +): ExecutorInfo[] | null { + try { + const packageJson = readAndCacheJsonFile( + join(collectionName, 'package.json'), + basedir + ); + + const collection = readAndCacheJsonFile( + packageJson.json.builders || packageJson.json.executors, + dirname(packageJson.path) + ); + + return getBuilderPaths(collectionName, collection.path, collection.json); + } catch (e) { + return null; + } +} + +function getBuilderPaths( + collectionName: string, + path: string, + json: any +): ExecutorInfo[] { + const baseDir = dirname(path); + + const builders: ExecutorInfo[] = []; + for (const [key, value] of Object.entries( + json.builders || json.executors + )) { + builders.push({ + name: `${collectionName}:${key}`, + path: join(baseDir, value.schema), + }); + } + + return builders; +} diff --git a/libs/vscode/json-schema/src/lib/workspace-json-schema.ts b/libs/vscode/json-schema/src/lib/workspace-json-schema.ts new file mode 100644 index 0000000000..1424692b0d --- /dev/null +++ b/libs/vscode/json-schema/src/lib/workspace-json-schema.ts @@ -0,0 +1,185 @@ +import { watchFile } from '@nx-console/server'; +import { WorkspaceConfigurationStore } from '@nx-console/vscode/configuration'; +import { dirname, join } from 'path'; +import * as vscode from 'vscode'; +import { ExecutorInfo, getAllExecutors } from './get-all-executors'; + +let FILE_WATCHER: vscode.FileSystemWatcher; + +export class WorkspaceJsonSchema { + constructor(context: vscode.ExtensionContext) { + const workspacePath = WorkspaceConfigurationStore.instance.get( + 'nxWorkspaceJsonPath', + '' + ); + + if (FILE_WATCHER) { + FILE_WATCHER.dispose(); + } + + /** + * Whenever a new package is added to the package.json, we recreate the schema. + * This allows newly added plugins to be added + */ + FILE_WATCHER = watchFile( + join(dirname(workspacePath), 'package.json'), + () => { + this.setupSchema(workspacePath, context.extensionUri, true); + } + ); + context.subscriptions.push(FILE_WATCHER); + + this.setupSchema(workspacePath, context.extensionUri); + } + + setupSchema( + workspacePath: string, + extensionUri: vscode.Uri, + clearPackageJsonCache = false + ) { + const filePath = vscode.Uri.joinPath(extensionUri, 'workspace-schema.json'); + const collections = getAllExecutors(workspacePath, clearPackageJsonCache); + const contents = getWorkspaceJsonSchema(collections); + vscode.workspace.fs.writeFile( + filePath, + new Uint8Array(Buffer.from(contents, 'utf8')) + ); + } +} + +function getWorkspaceJsonSchema(collections: ExecutorInfo[]) { + const [builders, executors] = createBuildersAndExecutorsSchema(collections); + const contents = createJsonSchema(builders, executors); + return contents; +} + +function createBuildersAndExecutorsSchema( + collections: ExecutorInfo[] +): [string, string] { + const builders = collections + .map( + (collection) => ` +{ + "if": { + "properties": { "builder": { "const": "${collection.name}" } }, + "required": ["builder"] + }, + "then": { + "properties": { + "options": { + "$ref": "${collection.path}" + } + } + } +} +` + ) + .join(','); + + const executors = collections + .map( + (collection) => ` +{ + "if": { + "properties": { "executor": { "const": "${collection.name}" } }, + "required": ["executor"] + }, + "then": { + "properties": { + "options": { + "$ref": "${collection.path}" + } + } + } +} +` + ) + .join(','); + + return [builders, executors]; +} + +function createJsonSchema(builders: string, executors: string) { + return ` + { + "title": "JSON schema for Nx workspaces", + "id": "https://nx.dev", + "type": "object", + "properties": { + "version": { + "type": "number", + "enum": [1, 2] + } + }, + "allOf": [ + { + "if": { + "properties": { "version": { "const": 1 } }, + "required": ["version"] + }, + "then": { + "properties": { + "projects": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "architect": { + "additionalProperties": { + "type": "object", + "properties": { + "builder": { + "type": "string" + }, + "options": { + "type": "object" + } + }, + "allOf": [ + ${builders} + ] + } + } + } + } + } + } + } + }, + { + "if": { + "properties": { "version": { "const": 2 } }, + "required": ["version"] + }, + "then": { + "properties": { + "projects": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "targets": { + "additionalProperties": { + "type": "object", + "properties": { + "executor": { + "type": "string" + }, + "options": { + "type": "object" + } + }, + "allOf": [ + ${executors} + ] + } + } + } + } + } + } + } + } + ] + }`; +} diff --git a/libs/vscode/json-schema/tsconfig.json b/libs/vscode/json-schema/tsconfig.json new file mode 100644 index 0000000000..667a3463d1 --- /dev/null +++ b/libs/vscode/json-schema/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/vscode/json-schema/tsconfig.lib.json b/libs/vscode/json-schema/tsconfig.lib.json new file mode 100644 index 0000000000..ca4b70c39c --- /dev/null +++ b/libs/vscode/json-schema/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["**/*.spec.ts"], + "include": ["**/*.ts"] +} diff --git a/libs/vscode/json-schema/tsconfig.spec.json b/libs/vscode/json-schema/tsconfig.spec.json new file mode 100644 index 0000000000..1798b378a9 --- /dev/null +++ b/libs/vscode/json-schema/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/nx.json b/nx.json index d7e02bee9b..4b9899510a 100644 --- a/nx.json +++ b/nx.json @@ -44,6 +44,7 @@ "vscode-webview": { "tags": [] }, "vscode-nx-run-target-view": { "tags": [] }, "vscode-nx-project-view": { "tags": [] }, - "vscode-nx-commands-view": { "tags": [] } + "vscode-nx-commands-view": { "tags": [] }, + "vscode-json-schema": { "tags": [] } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 9cc8a07148..57760f6da8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -50,7 +50,8 @@ ], "@nx-console/vscode/nx-commands-view": [ "libs/vscode/nx-commands-view/src/index.ts" - ] + ], + "@nx-console/vscode/json-schema": ["libs/vscode/json-schema/src/index.ts"] } }, "exclude": ["node_modules", "tmp"]