diff --git a/.circleci/config.yml b/.circleci/config.yml index 81420e95..48df7cca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,9 +2,8 @@ version: 2 jobs: build: - docker: - - image: circleci/node:12-browsers + - image: circleci/node:14-browsers steps: - checkout diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 465f13ff..4a894843 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,10 @@ jobs: - name: Checkout uses: actions/checkout@v2 - - name: Use Node.js 10.x + - name: Use Node.js 14.x uses: actions/setup-node@v1 with: - node-version: 10.x + node-version: 14.x - name: Prepare run: | diff --git a/LICENSE b/LICENSE index 69e30871..a86a2500 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2019-2020 Johannes Hoppe +Copyright (c) 2019-2021 Johannes Hoppe Copyright (c) 2019 Minko Gechev Permission is hereby granted, free of charge, to any person obtaining a copy of diff --git a/src/__snapshots__/ng-add.spec.ts.snap b/src/__snapshots__/ng-add.spec.ts.snap new file mode 100644 index 00000000..b4d0a916 --- /dev/null +++ b/src/__snapshots__/ng-add.spec.ts.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ng-add generating files generates new files if starting from scratch 1`] = ` +"{ + \\"version\\": 1, + \\"defaultProject\\": \\"THEPROJECT\\", + \\"projects\\": { + \\"THEPROJECT\\": { + \\"projectType\\": \\"application\\", + \\"root\\": \\"PROJECTROOT\\", + \\"architect\\": { + \\"build\\": { + \\"options\\": { + \\"outputPath\\": \\"dist/THEPROJECT\\" + } + }, + \\"deploy\\": { + \\"builder\\": \\"@angular-schule/ngx-deploy-starter:deploy\\" + } + } + }, + \\"OTHERPROJECT\\": { + \\"projectType\\": \\"application\\", + \\"root\\": \\"PROJECTROOT\\", + \\"architect\\": { + \\"build\\": { + \\"options\\": { + \\"outputPath\\": \\"dist/OTHERPROJECT\\" + } + } + } + } + } +}" +`; + +exports[`ng-add generating files overrides existing files 1`] = ` +"{ + \\"version\\": 1, + \\"defaultProject\\": \\"THEPROJECT\\", + \\"projects\\": { + \\"THEPROJECT\\": { + \\"projectType\\": \\"application\\", + \\"root\\": \\"PROJECTROOT\\", + \\"architect\\": { + \\"build\\": { + \\"options\\": { + \\"outputPath\\": \\"dist/THEPROJECT\\" + } + }, + \\"deploy\\": { + \\"builder\\": \\"@angular-schule/ngx-deploy-starter:deploy\\" + } + } + }, + \\"OTHERPROJECT\\": { + \\"projectType\\": \\"application\\", + \\"root\\": \\"PROJECTROOT\\", + \\"architect\\": { + \\"build\\": { + \\"options\\": { + \\"outputPath\\": \\"dist/OTHERPROJECT\\" + } + }, + \\"deploy\\": { + \\"builder\\": \\"@angular-schule/ngx-deploy-starter:deploy\\" + } + } + } + } +}" +`; diff --git a/src/deploy/actions.ts b/src/deploy/actions.ts index 58e827c5..16d02aa9 100644 --- a/src/deploy/actions.ts +++ b/src/deploy/actions.ts @@ -2,7 +2,7 @@ import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; -import { json, logging } from '@angular-devkit/core'; +import { logging } from '@angular-devkit/core'; import { Schema } from './schema'; import { BuildTarget } from '../interfaces'; @@ -31,8 +31,8 @@ export default async function deploy( ...(options.baseHref && { baseHref: options.baseHref }) }; - context.logger.info(`� Building "${context.target.project}"`); - context.logger.info(`� Build target "${buildTarget.name}"`); + context.logger.info(`📦 Building "${context.target.project}"`); + context.logger.info(`📦 Build target "${buildTarget.name}"`); const build = await context.scheduleTarget( targetFromTargetString(buildTarget.name), diff --git a/src/deploy/schema.json b/src/deploy/schema.json index 828f8b8c..2cef8f52 100644 --- a/src/deploy/schema.json +++ b/src/deploy/schema.json @@ -1,5 +1,5 @@ { - "id": "Schema", + "$id": "Schema", "title": "schema", "description": "Deployment of Angular CLI applications to the file system", "properties": { diff --git a/src/ng-add-schema.json b/src/ng-add-schema.json index 44565b31..3f611291 100644 --- a/src/ng-add-schema.json +++ b/src/ng-add-schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "ngx-deploy-starter-ng-add-schematic", + "$id": "ngx-deploy-starter-ng-add-schematic", "title": "ngx-deploy-starter ng-add schematic", "type": "object", "properties": {}, diff --git a/src/ng-add.spec.ts b/src/ng-add.spec.ts index 855718cf..1c787389 100644 --- a/src/ng-add.spec.ts +++ b/src/ng-add.spec.ts @@ -2,10 +2,9 @@ import { SchematicContext, Tree } from '@angular-devkit/schematics'; import { ngAdd } from './ng-add'; -const PROJECT_NAME = 'pie-ka-chu'; -const PROJECT_ROOT = 'pirojok'; - -const OTHER_PROJECT_NAME = 'pi-catch-you'; +const PROJECT_NAME = 'THEPROJECT'; +const PROJECT_ROOT = 'PROJECTROOT'; +const OTHER_PROJECT_NAME = 'OTHERPROJECT'; describe('ng-add', () => { describe('generating files', () => { @@ -17,118 +16,124 @@ describe('ng-add', () => { }); it('generates new files if starting from scratch', async () => { - const result = ngAdd({ + const result = await ngAdd({ project: PROJECT_NAME })(tree, {} as SchematicContext); - expect(result.read('angular.json')!.toString()).toEqual( - initialAngularJson - ); + const actual = result.read('angular.json')!.toString(); + expect(prettifyJSON(actual)).toMatchSnapshot(); }); it('overrides existing files', async () => { - const tempTree = ngAdd({ + const tempTree = await ngAdd({ project: PROJECT_NAME })(tree, {} as SchematicContext); - const result = ngAdd({ + const result = await ngAdd({ project: OTHER_PROJECT_NAME })(tempTree, {} as SchematicContext); const actual = result.read('angular.json')!.toString(); - expect(actual).toEqual(overwriteAngularJson); + expect(prettifyJSON(actual)).toMatchSnapshot(); }); }); describe('error handling', () => { - it('fails if project not defined', () => { + it('should fail if project not defined', async () => { const tree = Tree.empty(); const angularJSON = generateAngularJson(); delete angularJSON.defaultProject; tree.create('angular.json', JSON.stringify(angularJSON)); - expect(() => + await expect( ngAdd({ project: '' })(tree, {} as SchematicContext) - ).toThrowError( + ).rejects.toThrowError( 'No Angular project selected and no default project in the workspace' ); }); - it('Should throw if angular.json not found', async () => { - expect(() => + it('should throw if angular.json not found', async () => { + await expect( ngAdd({ project: PROJECT_NAME })(Tree.empty(), {} as SchematicContext) - ).toThrowError('Could not find angular.json'); + ).rejects.toThrowError('Unable to determine format for workspace path.'); }); - it('Should throw if angular.json can not be parsed', async () => { + it('should throw if angular.json can not be parsed', async () => { const tree = Tree.empty(); tree.create('angular.json', 'hi'); - expect(() => + await expect( ngAdd({ project: PROJECT_NAME })(tree, {} as SchematicContext) - ).toThrowError('Could not parse angular.json'); + ).rejects.toThrowError('Invalid JSON character: "h" at 0:0.'); }); - it('Should throw if specified project does not exist ', async () => { + it('should throw if specified project does not exist', async () => { const tree = Tree.empty(); - tree.create('angular.json', JSON.stringify({ projects: {} })); + tree.create('angular.json', JSON.stringify({ version: 1, projects: {} })); - expect(() => + await expect( ngAdd({ project: PROJECT_NAME })(tree, {} as SchematicContext) - ).toThrowError( + ).rejects.toThrowError( 'The specified Angular project is not defined in this workspace' ); }); - it('Should throw if specified project is not application', async () => { + it('should throw if specified project is not application', async () => { const tree = Tree.empty(); tree.create( 'angular.json', JSON.stringify({ - projects: { [PROJECT_NAME]: { projectType: 'pokemon' } } + version: 1, + projects: { [PROJECT_NAME]: { projectType: 'invalid' } } }) ); - expect(() => + await expect( ngAdd({ project: PROJECT_NAME })(tree, {} as SchematicContext) - ).toThrowError( + ).rejects.toThrowError( 'Deploy requires an Angular project type of "application" in angular.json' ); }); - it('Should throw if app does not have architect configured', async () => { + it('should throw if app does not have architect configured', async () => { const tree = Tree.empty(); tree.create( 'angular.json', JSON.stringify({ + version: 1, projects: { [PROJECT_NAME]: { projectType: 'application' } } }) ); - expect(() => + await expect( ngAdd({ project: PROJECT_NAME })(tree, {} as SchematicContext) - ).toThrowError( - 'Cannot read the output path (architect.build.options.outputPath) of the Angular project "pie-ka-chu" in angular.json' + ).rejects.toThrowError( + 'Cannot read the output path (architect.build.options.outputPath) of the Angular project "THEPROJECT" in angular.json' ); }); }); }); +function prettifyJSON(json: string) { + return JSON.stringify(JSON.parse(json), null, 2); +} + function generateAngularJson() { return { + version: 1, defaultProject: PROJECT_NAME as string | undefined, projects: { [PROJECT_NAME]: { @@ -137,7 +142,7 @@ function generateAngularJson() { architect: { build: { options: { - outputPath: 'dist/ikachu' + outputPath: 'dist/' + PROJECT_NAME } } } @@ -148,7 +153,7 @@ function generateAngularJson() { architect: { build: { options: { - outputPath: 'dist/ikachu' + outputPath: 'dist/' + OTHER_PROJECT_NAME } } } @@ -156,71 +161,3 @@ function generateAngularJson() { } }; } - -const initialAngularJson = `{ - "defaultProject": "pie-ka-chu", - "projects": { - "pie-ka-chu": { - "projectType": "application", - "root": "pirojok", - "architect": { - "build": { - "options": { - "outputPath": "dist/ikachu" - } - }, - "deploy": { - "builder": "@angular-schule/ngx-deploy-starter:deploy", - "options": {} - } - } - }, - "pi-catch-you": { - "projectType": "application", - "root": "pirojok", - "architect": { - "build": { - "options": { - "outputPath": "dist/ikachu" - } - } - } - } - } -}`; - -const overwriteAngularJson = `{ - "defaultProject": "pie-ka-chu", - "projects": { - "pie-ka-chu": { - "projectType": "application", - "root": "pirojok", - "architect": { - "build": { - "options": { - "outputPath": "dist/ikachu" - } - }, - "deploy": { - "builder": "@angular-schule/ngx-deploy-starter:deploy", - "options": {} - } - } - }, - "pi-catch-you": { - "projectType": "application", - "root": "pirojok", - "architect": { - "build": { - "options": { - "outputPath": "dist/ikachu" - } - }, - "deploy": { - "builder": "@angular-schule/ngx-deploy-starter:deploy", - "options": {} - } - } - } - } -}`; diff --git a/src/ng-add.ts b/src/ng-add.ts index 7a33afbc..aaf1c406 100644 --- a/src/ng-add.ts +++ b/src/ng-add.ts @@ -1,46 +1,25 @@ -import { JsonParseMode, parseJson } from '@angular-devkit/core'; +import { workspaces } from '@angular-devkit/core'; import { SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; -import { Workspace } from './interfaces'; +import { createHost } from './utils'; -function getWorkspace(host: Tree): { path: string; workspace: Workspace } { - const possibleFiles = ['/angular.json', '/.angular.json']; - const path = possibleFiles.filter(path => host.exists(path))[0]; - - const configBuffer = host.read(path); - if (configBuffer === null) { - throw new SchematicsException(`Could not find angular.json`); - } - const content = configBuffer.toString(); - - let workspace: Workspace; - try { - workspace = (parseJson(content, JsonParseMode.Loose) as {}) as Workspace; - } catch (e) { - throw new SchematicsException(`Could not parse angular.json: ` + e.message); - } - - return { - path, - workspace - }; -} interface NgAddOptions { project: string; } -export const ngAdd = (options: NgAddOptions) => ( +export const ngAdd = (options: NgAddOptions) => async ( tree: Tree, _context: SchematicContext ) => { - const { path: workspacePath, workspace } = getWorkspace(tree); + const host = createHost(tree); + const { workspace } = await workspaces.readWorkspace('/', host); if (!options.project) { - if (workspace.defaultProject) { - options.project = workspace.defaultProject; + if (workspace.extensions.defaultProject) { + options.project = workspace.extensions.defaultProject as string; } else { throw new SchematicsException( 'No Angular project selected and no default project in the workspace' @@ -48,35 +27,31 @@ export const ngAdd = (options: NgAddOptions) => ( } } - const project = workspace.projects[options.project]; + const project = workspace.projects.get(options.project); if (!project) { throw new SchematicsException( 'The specified Angular project is not defined in this workspace' ); } - if (project.projectType !== 'application') { + if (project.extensions.projectType !== 'application') { throw new SchematicsException( `Deploy requires an Angular project type of "application" in angular.json` ); } - if ( - !project.architect || - !project.architect.build || - !project.architect.build.options || - !project.architect.build.options.outputPath - ) { + if (!project.targets.get('build')?.options?.outputPath) { throw new SchematicsException( `Cannot read the output path (architect.build.options.outputPath) of the Angular project "${options.project}" in angular.json` ); } - project.architect['deploy'] = { + project.targets.add({ + name: 'deploy', builder: '@angular-schule/ngx-deploy-starter:deploy', options: {} - }; + }); - tree.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); + workspaces.writeWorkspace(workspace, host); return tree; }; diff --git a/src/package-lock.json b/src/package-lock.json index 26bac9c7..6981c82a 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1,6 +1,6 @@ { "name": "@angular-schule/ngx-deploy-starter", - "version": "1.0.0-rc.1", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/package.json b/src/package.json index 3025ed72..226353ab 100644 --- a/src/package.json +++ b/src/package.json @@ -1,6 +1,6 @@ { "name": "@angular-schule/ngx-deploy-starter", - "version": "1.0.0-rc.1", + "version": "1.0.0", "description": "Deployment from the Angular CLI to the file system. This is a sample project that helps you to implement your own deployment builder (`ng deploy`) for the Angular CLI.", "main": "index.js", "scripts": { @@ -40,9 +40,9 @@ }, "homepage": "https://github.com/angular-schule/ngx-deploy-starter/#readme", "devDependencies": { - "@angular-devkit/architect": ">= 0.900 < 0.1200", - "@angular-devkit/core": "^9.0.0 || ^10.0.0 || ^11.0.0", - "@angular-devkit/schematics": "^9.0.0 || ^10.0.0 || ^11.0.0", + "@angular-devkit/architect": ">= 0.900 < 0.1400", + "@angular-devkit/core": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "@angular-devkit/schematics": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0", "@types/fs-extra": "^9.0.4", "@types/jest": "^26.0.15", "@types/node": "^14.14.7", @@ -57,9 +57,9 @@ "typescript": ">=4.0.0 <4.1.0" }, "peerDependencies": { - "@angular-devkit/architect": ">= 0.900 < 0.1200", - "@angular-devkit/core": "^9.0.0 || ^10.0.0 || ^11.0.0", - "@angular-devkit/schematics": "^9.0.0 || ^10.0.0 || ^11.0.0" + "@angular-devkit/architect": ">= 0.900 < 0.1400", + "@angular-devkit/core": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "@angular-devkit/schematics": "^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0" }, "dependencies": { "fs-extra": "^9.0.1" diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..f6dba37a --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,23 @@ +import { virtualFs, workspaces } from '@angular-devkit/core'; +import { SchematicsException, Tree } from '@angular-devkit/schematics'; + +export function createHost(tree: Tree): workspaces.WorkspaceHost { + return { + async readFile(path: string): Promise { + const data = tree.read(path); + if (!data) { + throw new SchematicsException('File not found.'); + } + return virtualFs.fileBufferToString(data); + }, + async writeFile(path: string, data: string): Promise { + return tree.overwrite(path, data); + }, + async isDirectory(path: string): Promise { + return !tree.exists(path) && tree.getDir(path).subfiles.length > 0; + }, + async isFile(path: string): Promise { + return tree.exists(path); + } + }; +}