├── .nvmrc ├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.css │ ├── app.component.html │ └── app.component.ts ├── locales │ ├── messages.json │ └── messages.de-CH.json ├── styles.css ├── favicon.ico ├── environments │ ├── environment.ts │ └── environment.development.ts ├── index.html └── main.ts ├── modules └── testing │ └── builder │ ├── projects │ └── hello-world-app │ │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── app │ │ │ ├── app.component.scss │ │ │ ├── app.routes.ts │ │ │ ├── app.config.server.ts │ │ │ ├── app.component.ts │ │ │ └── app.config.ts │ │ ├── styles.scss │ │ ├── favicon.ico │ │ ├── main.ts │ │ └── index.html │ │ ├── tsconfig.spec.json │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── angular.json │ └── src │ ├── index.ts │ ├── file-watching.ts │ ├── dev_prod_mode.ts │ ├── builder-harness_spec.ts │ ├── test-utils.ts │ └── jasmine-helpers.ts ├── projects └── angular-server-side-configuration │ ├── ng-env │ ├── ng-package.json │ └── src │ │ ├── public_api.ts │ │ └── public_api.spec.ts │ ├── process │ ├── ng-package.json │ └── src │ │ └── public_api.ts │ ├── builders │ ├── package.json │ ├── ngsscbuild │ │ ├── ngssc-context.ts │ │ ├── schema.ts │ │ ├── schema.json │ │ ├── variable-detector.ts │ │ ├── index.spec.ts │ │ ├── variable-detector.spec.ts │ │ └── index.ts │ ├── tsconfig.json │ └── builders.json │ ├── schematics │ ├── package.json │ ├── ng-add │ │ ├── schema.ts │ │ ├── schema.json │ │ ├── index.spec.ts │ │ └── index.ts │ ├── tsconfig.json │ ├── collection.json │ ├── migration.json │ └── ng-update │ │ ├── index.ts │ │ └── index.spec.ts │ ├── src │ ├── public-api.ts │ ├── ngssc.ts │ ├── insert.ts │ ├── glob-to-regexp.ts │ ├── insert.spec.ts │ └── glob-to-regexp.spec.ts │ ├── ng-package.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── .eslintrc.json │ └── package.json ├── test ├── ngssc.sh ├── Dockerfile.test ├── jasmine.js └── default.conf.template ├── .prettierignore ├── .vscode └── settings.json ├── go.mod ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── scripts ├── tsconfig.json ├── standard-version-updater.js ├── build-cli.mts └── build-lib.mts ├── tsconfig.node.json ├── .eslintrc.json ├── tsconfig.json ├── ngssc.schema.json ├── .gitignore ├── cli ├── substitute.go ├── insert.go ├── insertion_task.go ├── substitution_target.go ├── insertion_target.go ├── ngssc_config.go ├── helpers_test.go ├── main.go ├── substitution_task.go ├── substitute_test.go └── insert_test.go ├── .github └── workflows │ ├── continuous-integration.yml │ └── release.yml ├── go.sum ├── package.json ├── angular.json └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | v24.11.1 2 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

{{ title }} app is running!

2 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/ng-env/ng-package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/process/ng-package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/ngssc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ngssc insert --nginx 3 | ngssc substitute -e --nginx -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .angular 2 | coverage 3 | dist 4 | modules/testing/builder/projects 5 | -------------------------------------------------------------------------------- /src/locales/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "en-US", 3 | "translations": {} 4 | } 5 | -------------------------------------------------------------------------------- /src/locales/messages.de-CH.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": "de-CH", 3 | "translations": {} 4 | } 5 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyubisation/angular-server-side-configuration/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export * from './ngssc'; 2 | export * from './insert'; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "go.formatTool": "goimports" 4 | } 5 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/ng-env/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export var NG_ENV: { [name: string]: string } = (globalThis as any).NG_ENV || {}; 2 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = []; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | import 'angular-server-side-configuration/process'; 2 | 3 | export const environment = { 4 | title: process.env['TITLE'], 5 | }; 6 | -------------------------------------------------------------------------------- /src/environments/environment.development.ts: -------------------------------------------------------------------------------- 1 | import 'angular-server-side-configuration/process'; 2 | 3 | export const environment = { 4 | title: 'static title', 5 | }; 6 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyubisation/angular-server-side-configuration/HEAD/modules/testing/builder/projects/hello-world-app/src/favicon.ico -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/ng-add/schema.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | additionalEnvironmentVariables: string; 3 | /** Name of the project. */ 4 | project: string; 5 | } 6 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/ngsscbuild/ngssc-context.ts: -------------------------------------------------------------------------------- 1 | import type { Variant } from 'angular-server-side-configuration'; 2 | 3 | /** The context for ngssc. */ 4 | export interface NgsscContext { 5 | variant: Variant; 6 | variables: string[]; 7 | } 8 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.node.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/angular-server-side-configuration/schematics" 5 | }, 6 | "exclude": ["**/files/**/*", "**/*.spec.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ngssc 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/bmatcuk/doublestar v1.3.4 7 | github.com/urfave/cli v1.22.15 8 | ) 9 | 10 | require ( 11 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 12 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.node.json", 3 | "compilerOptions": { 4 | "outDir": "../../../dist/angular-server-side-configuration/builders", 5 | "rootDir": "." 6 | }, 7 | "exclude": ["**/files/**/*", "**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular-server-side-configuration", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | }, 7 | "allowedNonPeerDependencies": ["glob"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": ["node"] 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err)); 6 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/builders.json: -------------------------------------------------------------------------------- 1 | { 2 | "builders": { 3 | "ngsscbuild": { 4 | "implementation": "./ngsscbuild", 5 | "schema": "./ngsscbuild/schema.json", 6 | "description": "Build angular with defined builder and generates ngssc.json." 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/ngsscbuild/schema.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | additionalEnvironmentVariables: string[]; 3 | /** @deprecated Use buildTarget instead. */ 4 | browserTarget: string; 5 | buildTarget: string; 6 | filePattern: string | null; 7 | searchPattern?: string | null; 8 | } 9 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "esModuleInterop": true, 6 | "outDir": "./out-tsc/spec", 7 | "types": ["jasmine", "node"] 8 | }, 9 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": ["jasmine"] 7 | }, 8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/process/src/public_api.ts: -------------------------------------------------------------------------------- 1 | export const process = (function (self: any) { 2 | self = self || {}; 3 | self.process = self.process || {}; 4 | self.process.env = self.process.env || {}; 5 | return self.process; 6 | })(globalThis); 7 | 8 | declare global { 9 | var process: NodeJS.Process; 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": ["node"] 7 | }, 8 | "files": ["src/main.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../dist/dev-infra-scripts", 4 | "target": "es2022", 5 | "lib": ["es2022"], 6 | "moduleResolution": "node", 7 | "module": "es2022", 8 | "types": ["node"], 9 | "strict": true, 10 | "noEmit": true, 11 | "skipLibCheck": true, 12 | "downlevelIteration": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "esModuleInterop": true, 6 | "outDir": "../../out-tsc/spec", 7 | "types": ["jasmine", "node"] 8 | }, 9 | "include": ["**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/ng-env/src/public_api.spec.ts: -------------------------------------------------------------------------------- 1 | describe('ng-env', () => { 2 | it('should read NG_ENV', async () => { 3 | const ngEnv = { test: 'expected' }; 4 | (globalThis as any).NG_ENV = ngEnv; 5 | const { NG_ENV } = await import('./public_api'); 6 | expect(NG_ENV).toEqual(ngEnv); 7 | delete (globalThis as any).NG_ENV; 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, InjectionToken } from '@angular/core'; 2 | 3 | export const TITLE_TOKEN = new InjectionToken('injection-token'); 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'], 9 | }) 10 | export class AppComponent { 11 | public title: string = inject(TITLE_TOKEN); 12 | } 13 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": ["node"] 10 | }, 11 | "exclude": ["src/test.ts", "**/*.spec.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularServerSideConfiguration 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/app/app.config.server.ts: -------------------------------------------------------------------------------- 1 | import { provideServerRendering } from '@angular/ssr'; 2 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'; 3 | import { appConfig } from './app.config'; 4 | 5 | const serverConfig: ApplicationConfig = { 6 | providers: [provideServerRendering()], 7 | }; 8 | 9 | export const config = mergeApplicationConfig(appConfig, serverConfig); 10 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "ng-add": { 5 | "description": "Adds Angular Material to the application without affecting any templates", 6 | "factory": "./ng-add/index#ngAdd", 7 | "schema": "./ng-add/schema.json", 8 | "aliases": ["install"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ng17 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterOutlet } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | standalone: true, 8 | imports: [CommonModule, RouterOutlet], 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.scss'], 11 | }) 12 | export class AppComponent { 13 | title = 'ng17'; 14 | } 15 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/src/ngssc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Model for ngssc.json. 3 | * @public 4 | */ 5 | export interface Ngssc { 6 | /** The ngssc variant. */ 7 | variant: Variant; 8 | /** The environment variables to insert. */ 9 | environmentVariables: string[]; 10 | /** Pattern for files that should have variables inserted. */ 11 | filePattern?: string; 12 | } 13 | 14 | /** 15 | * Available angular-server-side-configuration variants. 16 | * @public 17 | */ 18 | export type Variant = 'process' | 'global' | 'NG_ENV'; 19 | -------------------------------------------------------------------------------- /test/Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/nginxinc/nginx-unprivileged:stable-alpine 2 | 3 | ENV TITLE="container title" 4 | 5 | # Install ngssc binary 6 | ADD --chmod=0755 dist/cli/ngssc_64bit /usr/sbin/ngssc 7 | 8 | # Add configuration template 9 | #COPY test/default.conf.template /etc/nginx/conf.d/ 10 | COPY test/default.conf.template /etc/nginx/ngssc-templates/ 11 | 12 | # Add ngssc init script 13 | COPY --chmod=0755 test/ngssc.sh /docker-entrypoint.d/ngssc.sh 14 | 15 | # Copy app 16 | COPY --chmod=0666 dist/ngssc-app/browser /usr/share/nginx/html 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'; 2 | import { importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; 3 | 4 | import { TITLE_TOKEN, AppComponent } from './app/app.component'; 5 | import { environment } from './environments/environment'; 6 | 7 | bootstrapApplication(AppComponent, { 8 | providers: [ 9 | provideZoneChangeDetection(), 10 | importProvidersFrom(BrowserModule), 11 | { provide: TITLE_TOKEN, useValue: environment.title }, 12 | ], 13 | }).catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /modules/testing/builder/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.dev/license 7 | */ 8 | 9 | export { 10 | BuilderHarness, 11 | type BuilderHarnessExecutionOptions, 12 | type BuilderHarnessExecutionResult, 13 | } from './builder-harness'; 14 | export { 15 | type HarnessFileMatchers, 16 | JasmineBuilderHarness, 17 | describeBuilder, 18 | expectLog, 19 | expectNoLog, 20 | } from './jasmine-helpers'; 21 | export * from './test-utils'; 22 | -------------------------------------------------------------------------------- /test/jasmine.js: -------------------------------------------------------------------------------- 1 | const Jasmine = require('jasmine'); 2 | const path = require('path'); 3 | const tsconfig = require('../tsconfig.node.json'); 4 | 5 | if (module === require.main) { 6 | tsconfig.compilerOptions.types.push('jasmine', 'dom'); 7 | 8 | const jasmine = new Jasmine({ projectBaseDir: path.resolve() }); 9 | jasmine.exitOnCompletion = true; 10 | jasmine.addMatchingSpecFiles(['projects/angular-server-side-configuration/**/*.spec.ts']); 11 | jasmine 12 | .execute() 13 | .then((result) => { 14 | if (result.failedExpectations.length) { 15 | console.error(result.failedExpectations); 16 | } 17 | }) 18 | .catch((e) => console.error(e)); 19 | } 20 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/ng-add/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "angular-server-side-configuration-ng-add", 4 | "title": "angular-server-side-configuration ng-add schematic", 5 | "type": "object", 6 | "properties": { 7 | "project": { 8 | "type": "string", 9 | "description": "Name of the project.", 10 | "$default": { 11 | "$source": "projectName" 12 | } 13 | }, 14 | "additionalEnvironmentVariables": { 15 | "description": "Additional environment variables that should be added to ngssc.json", 16 | "type": "string", 17 | "x-prompt": "Add additional environment variables (comma-separated):" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/migration.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "migration-v15": { 5 | "version": "15-next", 6 | "description": "Updates angular-server-side-configuration to v15", 7 | "factory": "./ng-update/index#updateToV15" 8 | }, 9 | "migration-v17": { 10 | "version": "17-next", 11 | "description": "Updates angular-server-side-configuration to v17", 12 | "factory": "./ng-update/index#updateToV17" 13 | }, 14 | "dockerfile": { 15 | "version": "21.0.2", 16 | "description": "Updates the download url for ngssc", 17 | "factory": "./ng-update/index#dockerfile" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "angular-server-side-configuration": ["./dist/angular-server-side-configuration"], 5 | "angular-server-side-configuration/*": ["./dist/angular-server-side-configuration/*"] 6 | }, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "lib": ["es2022"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "outDir": "./out-tsc/node", 13 | "noEmitOnError": false, 14 | "strictNullChecks": true, 15 | "noImplicitOverride": true, 16 | "noImplicitReturns": true, 17 | "noImplicitAny": true, 18 | "skipDefaultLibCheck": true, 19 | "noFallthroughCasesInSwitch": true, 20 | "noUnusedLocals": false, 21 | "noImplicitThis": true, 22 | "skipLibCheck": true, 23 | "strictFunctionTypes": true, 24 | "sourceMap": true, 25 | "target": "es2022", 26 | "types": ["node"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": [ 9 | "projects/angular-server-side-configuration/tsconfig.lib.json", 10 | "projects/angular-server-side-configuration/tsconfig.spec.json" 11 | ], 12 | "createDefaultProgram": true 13 | }, 14 | "rules": { 15 | "@angular-eslint/directive-selector": [ 16 | "error", 17 | { 18 | "type": "attribute", 19 | "prefix": "lib", 20 | "style": "camelCase" 21 | } 22 | ], 23 | "@angular-eslint/component-selector": [ 24 | "error", 25 | { 26 | "type": "element", 27 | "prefix": "lib", 28 | "style": "kebab-case" 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "files": ["*.html"], 35 | "rules": {} 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /test/default.conf.template: -------------------------------------------------------------------------------- 1 | tcp_nopush on; 2 | tcp_nodelay on; 3 | types_hash_max_size 2048; 4 | 5 | # Example ${MANUAL_KEY} 6 | server { 7 | listen 8080 default_server; 8 | server_name _; 9 | add_header Content-Security-Policy "object-src 'none'; script-src 'self'; script-src-elem 'self' ${NGSSC_CSP_HASH}; base-uri 'self'; require-trusted-types-for 'script';" always; 10 | root /usr/share/nginx/html; 11 | 12 | location / { 13 | expires -1; 14 | add_header Pragma "no-cache"; 15 | add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0"; 16 | try_files $uri $uri/ /index.html =404; 17 | } 18 | 19 | location ~* \.(?:manifest|appcache|html?|xml|json)$ { 20 | expires -1; 21 | } 22 | 23 | location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ { 24 | expires 1M; 25 | access_log off; 26 | add_header Cache-Control "public"; 27 | } 28 | 29 | location ~* \.(?:css|js)$ { 30 | expires 1y; 31 | access_log off; 32 | add_header Cache-Control "public"; 33 | } 34 | } -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-server-side-configuration", 3 | "version": "21.0.2", 4 | "description": "Configure an angular application on the server", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/kyubisation/angular-server-side-configuration.git" 8 | }, 9 | "keywords": [ 10 | "angular", 11 | "configuration", 12 | "server", 13 | "server-side", 14 | "docker", 15 | "openshift", 16 | "kubernetes" 17 | ], 18 | "author": "kyubisation", 19 | "license": "Apache-2.0", 20 | "bugs": { 21 | "url": "https://github.com/kyubisation/angular-server-side-configuration/issues" 22 | }, 23 | "homepage": "https://github.com/kyubisation/angular-server-side-configuration#readme", 24 | "builders": "./builders/builders.json", 25 | "schematics": "./schematics/collection.json", 26 | "ng-update": { 27 | "migrations": "./schematics/migration.json" 28 | }, 29 | "peerDependencies": { 30 | "@angular/core": "^21.0.0" 31 | }, 32 | "dependencies": { 33 | "glob": "^10.0.0" 34 | } 35 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/recommended", 13 | "plugin:@angular-eslint/template/process-inline-templates", 14 | "plugin:import-x/recommended" 15 | ], 16 | "rules": { 17 | "@angular-eslint/directive-selector": [ 18 | "error", 19 | { 20 | "type": "attribute", 21 | "prefix": "app", 22 | "style": "camelCase" 23 | } 24 | ], 25 | "@angular-eslint/component-selector": [ 26 | "error", 27 | { 28 | "type": "element", 29 | "prefix": "app", 30 | "style": "kebab-case" 31 | } 32 | ], 33 | "import-x/no-unresolved": "off", 34 | "import-x/order": "error" 35 | } 36 | }, 37 | { 38 | "files": ["*.html"], 39 | "extends": ["plugin:@angular-eslint/template/recommended"], 40 | "rules": {} 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "paths": { 6 | "angular-server-side-configuration": ["dist/angular-server-side-configuration"], 7 | "angular-server-side-configuration/*": ["dist/angular-server-side-configuration/*"] 8 | }, 9 | "baseUrl": "./", 10 | "outDir": "./dist/out-tsc", 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "strict": true, 14 | "noImplicitOverride": true, 15 | "noPropertyAccessFromIndexSignature": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "sourceMap": true, 19 | "declaration": false, 20 | "experimentalDecorators": true, 21 | "moduleResolution": "bundler", 22 | "importHelpers": true, 23 | "target": "ES2022", 24 | "module": "ES2022", 25 | "useDefineForClassFields": false, 26 | "skipLibCheck": true 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import 'angular-server-side-configuration/process'; 2 | 3 | /** 4 | * How to use angular-server-side-configuration: 5 | * 6 | * Use process.env['NAME_OF_YOUR_ENVIRONMENT_VARIABLE'] 7 | * 8 | * const stringValue = process.env['STRING_VALUE']; 9 | * const stringValueWithDefault = process.env['STRING_VALUE'] || 'defaultValue'; 10 | * const numberValue = Number(process.env['NUMBER_VALUE']); 11 | * const numberValueWithDefault = Number(process.env['NUMBER_VALUE'] || 10); 12 | * const booleanValue = process.env['BOOLEAN_VALUE'] === 'true'; 13 | * const booleanValueInverted = process.env['BOOLEAN_VALUE_INVERTED'] !== 'false'; 14 | * const complexValue = JSON.parse(process.env['COMPLEX_JSON_VALUE]); 15 | * 16 | * Please note that process.env[variable] cannot be resolved. Please directly use strings. 17 | */ 18 | 19 | import { ApplicationConfig } from '@angular/core'; 20 | import { provideRouter } from '@angular/router'; 21 | 22 | import { routes } from './app.routes'; 23 | import { provideClientHydration } from '@angular/platform-browser'; 24 | 25 | export const appConfig: ApplicationConfig = { 26 | providers: [provideRouter(routes), provideClientHydration()], 27 | }; 28 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "esModuleInterop": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": ["ES2022", "dom"], 23 | "paths": { 24 | "angular-server-side-configuration": ["../../dist/angular-server-side-configuration"], 25 | "angular-server-side-configuration/*": ["../../dist/angular-server-side-configuration/*"] 26 | } 27 | }, 28 | "angularCompilerOptions": { 29 | "enableI18nLegacyMessageIdFormat": false, 30 | "strictInjectionParameters": true, 31 | "strictInputAccessModifiers": true, 32 | "strictTemplates": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ngssc.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": {}, 3 | "$schema": "http://json-schema.org/draft-07/schema#", 4 | "$id": "https://github.com/kyubisation/angular-server-side-configuration/blob/master/ngssc.schema.json", 5 | "type": "object", 6 | "title": "The Ngssc Schema", 7 | "required": ["variant", "environmentVariables"], 8 | "properties": { 9 | "variant": { 10 | "$id": "#/properties/variant", 11 | "type": "string", 12 | "title": "The Variant Schema", 13 | "default": "", 14 | "examples": ["process"], 15 | "pattern": "^(process|NG_ENV)$" 16 | }, 17 | "environmentVariables": { 18 | "$id": "#/properties/environmentVariables", 19 | "type": "array", 20 | "title": "The Environmentvariables Schema", 21 | "items": { 22 | "$id": "#/properties/environmentVariables/items", 23 | "type": "string", 24 | "title": "The Items Schema", 25 | "default": "", 26 | "examples": ["TEST_VALUE"], 27 | "pattern": "^([\\w_]*)$" 28 | } 29 | }, 30 | "filePattern": { 31 | "$id": "#/properties/filePattern", 32 | "type": "string", 33 | "title": "The Filepattern Schema", 34 | "default": "", 35 | "examples": ["**/index.html"], 36 | "pattern": "^(.*)$" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /scripts/standard-version-updater.js: -------------------------------------------------------------------------------- 1 | module.exports.readVersion = function (_contents) { 2 | return require('../package.json').version; 3 | }; 4 | 5 | module.exports.writeVersion = function (contents, version) { 6 | try { 7 | const json = JSON.parse(contents); 8 | if (json.name && json.version) { 9 | // projects/angular-server-side-configuration/package.json 10 | json.version = version; 11 | const majorVersionRange = `^${version.split('.')[0]}.0.0`; 12 | for (const name of Object.keys(json.peerDependencies).filter((n) => 13 | n.startsWith('@angular/'), 14 | )) { 15 | json.peerDependencies[name] = majorVersionRange; 16 | } 17 | } else { 18 | // projects/angular-server-side-configuration/schematics/migration.json 19 | json.schematics.dockerfile.version = version; 20 | } 21 | return JSON.stringify(json, null, 2); 22 | } catch { 23 | return contents.replace( 24 | /https:\/\/github.com\/kyubisation\/angular-server-side-configuration\/releases\/download\/v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?/g, 25 | `https://github.com/kyubisation/angular-server-side-configuration/releases/download/v${version}`, 26 | ); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/ngsscbuild/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "type": "object", 4 | "properties": { 5 | "additionalEnvironmentVariables": { 6 | "type": "array", 7 | "description": "Additional environment variables that should be added to ngssc.json" 8 | }, 9 | "browserTarget": { 10 | "type": "string", 11 | "description": "A browser builder target to build in the format of `project:target[:configuration]`.", 12 | "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$", 13 | "x-deprecated": "Use 'buildTarget' instead." 14 | }, 15 | "buildTarget": { 16 | "type": "string", 17 | "description": "A build builder target to build in the format of `project:target[:configuration]`.", 18 | "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" 19 | }, 20 | "filePattern": { 21 | "type": "string", 22 | "description": "The file pattern, into which the environment variables should be inserted during ngssc insert (Defaults to index.html)", 23 | "default": "" 24 | }, 25 | "searchPattern": { 26 | "type": "string", 27 | "description": "The search pattern to use when searching for environment variable occurrences (Defaults to {sourceRoot}/**/environments/environment*.ts)", 28 | "default": "" 29 | } 30 | }, 31 | "anyOf": [{ "required": ["buildTarget"] }, { "required": ["browserTarget"] }] 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | test/test-project-* 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Golang coverage 25 | cli/coverage.out 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | 67 | # Angular 68 | .angular 69 | 70 | # Others 71 | .vscode 72 | .nx 73 | dist 74 | out-tsc 75 | junit.xml 76 | 77 | # Go 78 | cli/app 79 | cli/app.exe 80 | cli/pkg 81 | cli/vendor 82 | -------------------------------------------------------------------------------- /cli/substitute.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | // SubstituteCommand is the ngssc CLI command to insert environment variables 12 | func SubstituteCommand(c *cli.Context) error { 13 | // Init Flags 14 | nginxFlag := c.Bool("nginx") 15 | dryRun := c.Bool("dry") 16 | ngsscPath := c.String("ngssc-path") 17 | hashAlgorithm := c.String("hash-algorithm") 18 | out := c.String("out") 19 | includeEnv := c.Bool("include-env") 20 | 21 | // Dry Run Flag 22 | if dryRun { 23 | fmt.Println("DRY RUN! Files will not be changed!") 24 | } 25 | 26 | workingDirectory, err := os.Getwd() 27 | if err != nil { 28 | return fmt.Errorf("unable to resolve the current working directory.\n%v", err) 29 | } 30 | 31 | templateDirectory := workingDirectory 32 | if c.NArg() > 0 { 33 | templateDirectory, err = filepath.Abs(c.Args()[0]) 34 | if err != nil { 35 | return fmt.Errorf("unable to resolve the absolute path of %v\n%v", c.Args()[0], err) 36 | } 37 | } else if nginxFlag { 38 | templateDirectory = "/etc/nginx/ngssc-templates" 39 | } 40 | 41 | if ngsscPath == "" && nginxFlag { 42 | ngsscPath = "/usr/share/nginx/html/**/ngssc.json" 43 | } 44 | 45 | if out == "" && nginxFlag { 46 | out = "/etc/nginx/conf.d/" 47 | } 48 | 49 | task := &SubstitutionTask{ 50 | workingDirectory, 51 | templateDirectory, 52 | ngsscPath, 53 | hashAlgorithm, 54 | out, 55 | includeEnv, 56 | dryRun, 57 | } 58 | return task.Substitute() 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push, pull_request] 3 | 4 | concurrency: 5 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 6 | cancel-in-progress: true 7 | 8 | permissions: read-all 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: actions/setup-node@v6 16 | with: 17 | cache: yarn 18 | node-version-file: '.nvmrc' 19 | - run: yarn install --frozen-lockfile --non-interactive 20 | 21 | - name: 'Run eslint' 22 | run: yarn -s lint 23 | 24 | build-and-test-library: 25 | runs-on: ubuntu-latest 26 | needs: lint 27 | steps: 28 | - uses: actions/checkout@v6 29 | - uses: actions/setup-node@v6 30 | with: 31 | cache: yarn 32 | node-version-file: '.nvmrc' 33 | - run: yarn install --frozen-lockfile --non-interactive 34 | 35 | 36 | - name: 'Build library' 37 | run: yarn build:lib 38 | - name: 'Test library' 39 | run: yarn test:lib 40 | 41 | build-and-test-cli: 42 | runs-on: ubuntu-latest 43 | needs: lint 44 | steps: 45 | - uses: actions/checkout@v6 46 | - uses: actions/setup-node@v6 47 | with: 48 | cache: yarn 49 | node-version-file: '.nvmrc' 50 | - run: yarn install --frozen-lockfile --non-interactive 51 | - uses: actions/setup-go@v6 52 | with: 53 | go-version-file: 'go.mod' 54 | - run: go version 55 | 56 | - name: 'Build cli' 57 | run: yarn build:cli 58 | - name: 'Test cli' 59 | run: yarn test:cli 60 | -------------------------------------------------------------------------------- /cli/insert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | // InsertCommand is the ngssc CLI command to insert environment variables 12 | func InsertCommand(c *cli.Context) error { 13 | // Init Flags 14 | nginxFlag := c.Bool("nginx") 15 | noncePlaceholder := c.String("nonce") 16 | dryRunFlag := c.Bool("dry") 17 | recursive := c.Bool("recursive") 18 | if !recursive && nginxFlag { 19 | recursive = true 20 | } 21 | 22 | // Dry Run Flag 23 | if dryRunFlag { 24 | fmt.Println("DRY RUN! Files will not be changed!") 25 | } 26 | 27 | // Resolve target directory 28 | var workingDirectory string 29 | if c.NArg() > 0 { 30 | var err error 31 | workingDirectory, err = filepath.Abs(c.Args()[0]) 32 | if err != nil { 33 | return fmt.Errorf("unable to resolve the absolute path of %v\n%v", c.Args()[0], err) 34 | } 35 | } else if nginxFlag { 36 | workingDirectory = "/usr/share/nginx/html" 37 | } else { 38 | var err error 39 | workingDirectory, err = os.Getwd() 40 | if err != nil { 41 | return fmt.Errorf( 42 | "unable to resolve the current working directory. "+ 43 | "Please specify the directory as a CLI parameter. (e.g. ngssc insert /path/to/directory)\n%v", 44 | err) 45 | } 46 | } 47 | 48 | fmt.Printf("Working directory:\n %v\n", workingDirectory) 49 | if _, err := os.Stat(workingDirectory); os.IsNotExist(err) { 50 | return fmt.Errorf("working directory does not exist\n%v", err) 51 | } 52 | 53 | task := InsertionTask{ 54 | path: workingDirectory, 55 | noncePlaceholder: noncePlaceholder, 56 | dryRun: dryRunFlag, 57 | } 58 | if recursive { 59 | return task.Recursive() 60 | } else { 61 | return task.Single() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /modules/testing/builder/src/file-watching.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.dev/license 7 | */ 8 | 9 | type BuilderWatcherCallback = ( 10 | events: Array<{ path: string; type: 'created' | 'modified' | 'deleted'; time?: number }>, 11 | ) => void; 12 | 13 | interface BuilderWatcherFactory { 14 | watch( 15 | files: Iterable, 16 | directories: Iterable, 17 | callback: BuilderWatcherCallback, 18 | ): { close(): void }; 19 | } 20 | 21 | class WatcherDescriptor { 22 | constructor( 23 | readonly files: ReadonlySet, 24 | readonly directories: ReadonlySet, 25 | readonly callback: BuilderWatcherCallback, 26 | ) {} 27 | 28 | shouldNotify(path: string): boolean { 29 | return true; 30 | } 31 | } 32 | 33 | export class WatcherNotifier implements BuilderWatcherFactory { 34 | private readonly descriptors = new Set(); 35 | 36 | notify(events: Iterable<{ path: string; type: 'modified' | 'deleted' }>): void { 37 | for (const descriptor of this.descriptors) { 38 | for (const { path } of events) { 39 | if (descriptor.shouldNotify(path)) { 40 | descriptor.callback([...events]); 41 | break; 42 | } 43 | } 44 | } 45 | } 46 | 47 | watch( 48 | files: Iterable, 49 | directories: Iterable, 50 | callback: BuilderWatcherCallback, 51 | ): { close(): void } { 52 | const descriptor = new WatcherDescriptor(new Set(files), new Set(directories), callback); 53 | this.descriptors.add(descriptor); 54 | 55 | return { close: () => this.descriptors.delete(descriptor) }; 56 | } 57 | } 58 | 59 | export type { BuilderWatcherFactory }; 60 | -------------------------------------------------------------------------------- /scripts/build-cli.mts: -------------------------------------------------------------------------------- 1 | import { exec } from 'node:child_process'; 2 | import { readdirSync, readFileSync, statSync } from 'node:fs'; 3 | import { cp } from 'node:fs/promises'; 4 | import { join } from 'node:path'; 5 | import { promisify } from 'node:util'; 6 | 7 | const root = new URL('../', import.meta.url).pathname; 8 | const dist = join(root, 'dist', 'cli'); 9 | const version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version; 10 | const cliDirectory = join(root, 'cli'); 11 | const binaries: { os: string; arch: string; fileName: string, alias?: string }[] = [ 12 | { os: 'windows', arch: 'amd64', fileName: 'ngssc_amd64.exe', alias: 'ngssc_64bit.exe' }, 13 | { os: 'linux', arch: 'amd64', fileName: 'ngssc_amd64', alias: 'ngssc_64bit' }, 14 | { os: 'linux', arch: 'arm64', fileName: 'ngssc_arm64' }, 15 | { os: 'darwin', arch: 'amd64', fileName: 'ngssc_darwin_amd64', alias: 'ngssc_darwin_64bit' }, 16 | { os: 'darwin', arch: 'arm64', fileName: 'ngssc_darwin_arm64' }, 17 | ]; 18 | 19 | const asyncExec = promisify(exec); 20 | await Promise.all( 21 | binaries.map(async (binary) => { 22 | const binaryDist = join(dist, binary.fileName); 23 | console.log(`Building for ${binary.os} ${binary.arch}`); 24 | await asyncExec( 25 | `go build -ldflags="-s -w -X main.CliVersion=${version}" -buildvcs=false -o ${binaryDist}`, 26 | { 27 | cwd: cliDirectory, 28 | env: { 29 | ...process.env, 30 | GOOS: binary.os, 31 | GOARCH: binary.arch, 32 | }, 33 | }, 34 | ); 35 | console.log(`Finished building for ${binary.os} ${binary.arch}: ${binary.fileName}`); 36 | if (binary.alias) { 37 | await cp(binaryDist, join(dist, binary.alias)); 38 | console.log(`Created alias for ${binary.os} ${binary.arch}: ${binary.alias}`); 39 | } 40 | }), 41 | ); 42 | 43 | console.table(readdirSync(dist).sort().reduce((current, next) => Object.assign(current, { [next]: statSync(join(dist, next)).size }), {} as Record)); 44 | -------------------------------------------------------------------------------- /modules/testing/builder/src/dev_prod_mode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.dev/license 7 | */ 8 | 9 | import { BuilderHarness } from './builder-harness'; 10 | 11 | export const GOOD_TARGET = './src/good.js'; 12 | export const BAD_TARGET = './src/bad.js'; 13 | 14 | /** Setup project for use of conditional imports. */ 15 | export async function setupConditionImport(harness: BuilderHarness) { 16 | // Files that can be used as targets for the conditional import. 17 | await harness.writeFile('src/good.ts', `export const VALUE = 'good-value';`); 18 | await harness.writeFile('src/bad.ts', `export const VALUE = 'bad-value';`); 19 | await harness.writeFile('src/wrong.ts', `export const VALUE = 1;`); 20 | 21 | // Simple application file that accesses conditional code. 22 | await harness.writeFile( 23 | 'src/main.ts', 24 | `import {VALUE} from '#target'; 25 | console.log(VALUE); 26 | console.log(VALUE.length); 27 | export default 42 as any; 28 | `, 29 | ); 30 | 31 | // Ensure that good/bad can be resolved from tsconfig. 32 | const tsconfig = JSON.parse(harness.readFile('src/tsconfig.app.json')) as TypeScriptConfig; 33 | tsconfig.compilerOptions.moduleResolution = 'bundler'; 34 | tsconfig.files.push('good.ts', 'bad.ts', 'wrong.ts'); 35 | await harness.writeFile('src/tsconfig.app.json', JSON.stringify(tsconfig)); 36 | } 37 | 38 | /** Update package.json with the given mapping for #target. */ 39 | export async function setTargetMapping(harness: BuilderHarness, mapping: unknown) { 40 | await harness.writeFile( 41 | 'package.json', 42 | JSON.stringify({ 43 | name: 'ng-test-app', 44 | imports: { 45 | '#target': mapping, 46 | }, 47 | }), 48 | ); 49 | } 50 | 51 | interface TypeScriptConfig { 52 | compilerOptions: { 53 | moduleResolution: string; 54 | }; 55 | files: string[]; 56 | } 57 | -------------------------------------------------------------------------------- /cli/insertion_task.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/bmatcuk/doublestar" 8 | ) 9 | 10 | // InsertionTask represents an insertion task 11 | type InsertionTask struct { 12 | path string 13 | noncePlaceholder string 14 | dryRun bool 15 | } 16 | 17 | // Single will perform the insertion for a single ngssc.json 18 | func (task InsertionTask) Single() error { 19 | ngsscConfig, err := NgsscJsonConfigFromPath(task.path) 20 | if err != nil { 21 | return err 22 | } 23 | return task.insertWithNgssc(ngsscConfig) 24 | } 25 | 26 | // Recursive will perform the insertion for all ngssc.json below given path 27 | func (task InsertionTask) Recursive() error { 28 | pattern := filepath.Join(task.path, "**", "ngssc.json") 29 | configs, err := FindNgsscJsonConfigs(pattern) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | for _, ngsscConfig := range configs { 35 | err = task.insertWithNgssc(ngsscConfig) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func (task InsertionTask) insertWithNgssc(ngsscConfig NgsscConfig) error { 45 | pattern := filepath.Join(filepath.Dir(ngsscConfig.FilePath), ngsscConfig.FilePattern) 46 | files, err := doublestar.Glob(pattern) 47 | if err != nil { 48 | return fmt.Errorf("unable to resolve pattern: %v\n%v", pattern, err) 49 | } else if files == nil { 50 | return fmt.Errorf("no files found with pattern: %v", ngsscConfig.FilePattern) 51 | } 52 | 53 | logInsertionDetails(files, ngsscConfig) 54 | if !task.dryRun { 55 | for _, insertionFile := range files { 56 | target := InsertionTarget{ 57 | filePath: insertionFile, 58 | noncePlaceholder: task.noncePlaceholder, 59 | ngsscConfig: ngsscConfig, 60 | } 61 | target.Insert() 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func logInsertionDetails(files []string, ngsscConfig NgsscConfig) { 69 | fmt.Println("Inserting variables:") 70 | fmt.Println(" Files:") 71 | for _, file := range files { 72 | fmt.Printf(" %v\n", file) 73 | } 74 | fmt.Printf(" Variant: %v\n", ngsscConfig.Variant) 75 | fmt.Printf(" Variables:\n") 76 | for key, value := range ngsscConfig.EnvironmentVariables { 77 | if value != nil { 78 | fmt.Printf(" %v: %v\n", key, *value) 79 | } else { 80 | fmt.Printf(" %v: %v\n", key, "null") 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 2 | github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= 3 | github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= 4 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 12 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 15 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 16 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 17 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 18 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 19 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 20 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 21 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 22 | github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM= 23 | github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 26 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | - 'v[0-9]+.[0-9]+.[0-9]+-*' 7 | 8 | jobs: 9 | release-cli: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: actions/setup-node@v6 16 | with: 17 | cache: yarn 18 | node-version-file: '.nvmrc' 19 | - run: yarn install --frozen-lockfile --non-interactive 20 | - uses: actions/setup-go@v6 21 | with: 22 | go-version-file: 'go.mod' 23 | - run: go version 24 | 25 | - name: 'Build cli' 26 | run: yarn build:cli 27 | 28 | - uses: actions/github-script@v8 29 | with: 30 | script: | 31 | const fs = require('fs'); 32 | 33 | const { owner, repo } = context.repo; 34 | const tagName = context.ref.split('/')[2]; 35 | console.log(`Creating release for ${owner} in ${repo} at ${tagName}`); 36 | const release = await github.rest.repos.createRelease({ 37 | owner, 38 | repo, 39 | tag_name: tagName, 40 | generate_release_notes: true, 41 | draft: true, 42 | }); 43 | 44 | for (const file of fs.readdirSync('./dist/cli')) { 45 | console.log(`Adding asset ${file}`); 46 | await github.rest.repos.uploadReleaseAsset({ 47 | owner, 48 | repo, 49 | release_id: release.data.id, 50 | name: file, 51 | data: await fs.readFileSync(`./dist/cli/${file}`), 52 | }); 53 | } 54 | 55 | release-library: 56 | runs-on: ubuntu-latest 57 | needs: release-cli 58 | permissions: 59 | id-token: write # Required for OIDC 60 | steps: 61 | - uses: actions/checkout@v6 62 | - uses: actions/setup-node@v6 63 | with: 64 | cache: yarn 65 | node-version-file: '.nvmrc' 66 | registry-url: 'https://registry.npmjs.org' 67 | - run: yarn install --frozen-lockfile --non-interactive 68 | 69 | - name: 'Build library' 70 | run: yarn build:lib 71 | 72 | - name: 'Publish: Determine npm tag' 73 | id: npm_tag 74 | run: | 75 | if [[ "$REF" == *"-"* ]] 76 | then 77 | echo "npm_tag=next" >> $GITHUB_OUTPUT 78 | else 79 | echo "npm_tag=latest" >> $GITHUB_OUTPUT 80 | fi 81 | env: 82 | REF: ${{ github.ref }} 83 | - name: 'Publish: angular-server-side-configuration' 84 | run: | 85 | npm install --global npm@latest 86 | cd dist/angular-server-side-configuration 87 | npm publish --tag ${{ steps.npm_tag.outputs.npm_tag }} 88 | -------------------------------------------------------------------------------- /cli/substitution_target.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "path/filepath" 7 | "regexp" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | type SubstitutionTarget struct { 13 | templateFile string 14 | targetFile string 15 | envMap map[string]*string 16 | dryRun bool 17 | } 18 | 19 | func createSubstitutionTarget(templateFile string, outDir string, envMap map[string]*string, dryRun bool) SubstitutionTarget { 20 | targetFile := templateFile[:len(templateFile)-len(".template")] 21 | if outDir != "" { 22 | targetFile = filepath.Join(outDir, filepath.Base(targetFile)) 23 | } 24 | substituteTarget := &SubstitutionTarget{ 25 | templateFile, 26 | targetFile, 27 | envMap, 28 | dryRun, 29 | } 30 | return *substituteTarget 31 | } 32 | 33 | func (target SubstitutionTarget) Substitute() error { 34 | content, err := ioutil.ReadFile(target.templateFile) 35 | if err != nil { 36 | return fmt.Errorf("failed to read %v\n%v", target.templateFile, err) 37 | } 38 | 39 | variableRegex := regexp.MustCompile(`\${[a-zA-Z0-9_]+}`) 40 | matches := variableRegex.FindAllString(string(content), -1) 41 | matches = removeDuplicates(matches) 42 | substitutionMap := createSubstitutionMap(matches, target.envMap) 43 | logSubstitutionDetails(target.templateFile, target.targetFile, matches, substitutionMap) 44 | if target.dryRun { 45 | return nil 46 | } 47 | 48 | stringContent := string(content) 49 | for key, value := range substitutionMap { 50 | stringContent = strings.ReplaceAll(stringContent, key, value) 51 | } 52 | ioutil.WriteFile(target.targetFile, []byte(stringContent), 0644) 53 | return nil 54 | } 55 | 56 | func removeDuplicates(strSlice []string) []string { 57 | allKeys := make(map[string]bool) 58 | list := []string{} 59 | for _, item := range strSlice { 60 | if _, value := allKeys[item]; !value { 61 | allKeys[item] = true 62 | list = append(list, item) 63 | } 64 | } 65 | sort.Strings(list) 66 | return list 67 | } 68 | 69 | func createSubstitutionMap(variables []string, envMap map[string]*string) map[string]string { 70 | substitutionMap := make(map[string]string) 71 | for _, variable := range variables { 72 | key := variable[2 : len(variable)-1] 73 | value, ok := envMap[key] 74 | if ok { 75 | substitutionMap[variable] = *value 76 | } 77 | } 78 | return substitutionMap 79 | } 80 | 81 | func logSubstitutionDetails(templateFile string, targetFile string, matches []string, substitutionMap map[string]string) { 82 | fmt.Println("Substituting variables:") 83 | fmt.Printf(" Source: %v\n", templateFile) 84 | fmt.Printf(" Target: %v\n", targetFile) 85 | fmt.Printf(" Variables: %v\n", strings.Join(matches, ", ")) 86 | if len(substitutionMap) == 0 { 87 | fmt.Println(" Substitutions: No substitutable variables") 88 | return 89 | } 90 | 91 | fmt.Println(" Substitutions:") 92 | for key, value := range substitutionMap { 93 | fmt.Printf(" %v: %v\n", key, value) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/ngsscbuild/variable-detector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createSourceFile, 3 | forEachChild, 4 | isElementAccessExpression, 5 | isIdentifier, 6 | isPropertyAccessExpression, 7 | isStringLiteralLike, 8 | type Node, 9 | ScriptTarget, 10 | isImportDeclaration, 11 | } from 'typescript'; 12 | import type { Variant } from 'angular-server-side-configuration'; 13 | import type { logging } from '@angular-devkit/core'; 14 | import type { NgsscContext } from './ngssc-context'; 15 | 16 | /** Detect environment variables in given file. */ 17 | export class VariableDetector { 18 | private _logger?: logging.LoggerApi; 19 | 20 | constructor(logger?: logging.LoggerApi) { 21 | this._logger = logger; 22 | } 23 | 24 | detect(fileContent: string): NgsscContext { 25 | const sourceFile = createSourceFile('environment.ts', fileContent, ScriptTarget.ESNext, true); 26 | let variant: Variant = 'process'; 27 | const ngEnvVariables: string[] = []; 28 | const processVariables: string[] = []; 29 | iterateNodes(sourceFile, (node) => { 30 | if ( 31 | isImportDeclaration(node) && 32 | node.moduleSpecifier.getText().match(/angular-server-side-configuration\/ng-env/) 33 | ) { 34 | variant = 'NG_ENV'; 35 | } else if (!isIdentifier(node)) { 36 | return; 37 | } 38 | if (node.getText() === 'NG_ENV') { 39 | const variable = this._extractVariable(node, (n) => n.parent); 40 | if (variable) { 41 | ngEnvVariables.push(variable); 42 | } 43 | } else if (node.getText() === 'process') { 44 | const variable = this._extractVariable(node, (n) => n.parent.parent); 45 | if (variable) { 46 | processVariables.push(variable); 47 | } 48 | } 49 | }); 50 | if (ngEnvVariables.length && processVariables.length) { 51 | this._logger?.warn( 52 | `Detected both process.env.* and NG_ENV.* variables with selected variant ${variant}. Only the variables matching the current variant will be used.`, 53 | ); 54 | } 55 | const variables = (variant === 'process' ? processVariables : ngEnvVariables).sort(); 56 | return { variables, variant }; 57 | } 58 | 59 | private _extractVariable(node: Node, resolveRoot: (n: Node) => Node) { 60 | if (!isPropertyAccessExpression(node.parent) && !isElementAccessExpression(node.parent)) { 61 | return undefined; 62 | } 63 | const root: Node = resolveRoot(node); 64 | if (isPropertyAccessExpression(root)) { 65 | return root.name.getText(); 66 | } 67 | if (isElementAccessExpression(root) && isStringLiteralLike(root.argumentExpression)) { 68 | return root.argumentExpression.getText().replace(/['"`]/g, ''); 69 | } 70 | 71 | this._logger?.warn( 72 | `Unable to resolve variable from ${node.getText()}. Please use direct assignment.`, 73 | ); 74 | return undefined; 75 | } 76 | } 77 | 78 | function iterateNodes(node: Node, action: (node: Node) => void) { 79 | action(node); 80 | forEachChild(node, (n) => iterateNodes(n, action)); 81 | } 82 | -------------------------------------------------------------------------------- /scripts/build-lib.mts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import { lstatSync, readdirSync, readFileSync, writeFileSync } from 'fs'; 3 | import { copyFile, mkdir, readFile } from 'fs/promises'; 4 | import * as glob from 'glob'; 5 | import { dirname, join, relative } from 'path'; 6 | 7 | await finalizePackage(); 8 | 9 | interface Schema { 10 | properties: Record; 11 | } 12 | 13 | async function finalizePackage() { 14 | const rootDir = new URL('..', import.meta.url).pathname; 15 | const sourceDir = join(rootDir, 'projects/angular-server-side-configuration'); 16 | const targetDir = join(rootDir, 'dist/angular-server-side-configuration'); 17 | const schemaDirs = ['builders', 'schematics'].map((d) => join(sourceDir, d)); 18 | execSync('yarn ng build angular-server-side-configuration', { cwd: rootDir, stdio: 'inherit' }); 19 | 20 | console.log(`Copying required assets:`); 21 | for (const file of ['README.md', 'LICENSE']) { 22 | console.log(` - ${file}`); 23 | await copyFile(join(rootDir, file), join(targetDir, file)); 24 | } 25 | for (const file of walk(schemaDirs, /.json$/).filter((f) => !f.endsWith('tsconfig.json'))) { 26 | const relativePath = relative(sourceDir, file); 27 | const targetPath = join(targetDir, relativePath); 28 | console.log(` - ${relativePath}`); 29 | await mkdir(dirname(targetPath), { recursive: true }); 30 | await copyFile(file, targetPath); 31 | } 32 | 33 | const ngsscSchema: Schema = JSON.parse( 34 | await readFile(join(sourceDir, 'builders/ngsscbuild/schema.json'), 'utf8'), 35 | ); 36 | delete ngsscSchema.properties['buildTarget']; 37 | delete ngsscSchema.properties['browserTarget']; 38 | 39 | for (const schemaDir of schemaDirs) { 40 | const relativeSchemaDir = relative(rootDir, schemaDir); 41 | console.log(`Building ${relativeSchemaDir}`); 42 | execSync(`yarn tsc --project ${relativeSchemaDir}/tsconfig.json`, { 43 | cwd: rootDir, 44 | stdio: 'inherit', 45 | }); 46 | } 47 | 48 | const packageJsonPath = join(targetDir, 'package.json'); 49 | const distPackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); 50 | distPackageJson.sideEffects = glob 51 | .sync(['esm*/**/public_api.{mjs,js}', 'fesm*/*{ng-env,process}.{mjs,js}'], { 52 | cwd: targetDir, 53 | dotRelative: true, 54 | }) 55 | .sort(); 56 | writeFileSync(packageJsonPath, JSON.stringify(distPackageJson, null, 2), 'utf8'); 57 | } 58 | 59 | function walk(root: string | string[], fileRegex: RegExp): string[] { 60 | if (Array.isArray(root)) { 61 | return root.reduce((current, next) => current.concat(walk(next, fileRegex)), [] as string[]); 62 | } 63 | 64 | const directory = root.replace(/\\/g, '/'); 65 | return readdirSync(directory) 66 | .map((f) => `${directory}/${f}`) 67 | .map((f) => { 68 | const stat = lstatSync(f); 69 | if (stat.isDirectory()) { 70 | return walk(f, fileRegex); 71 | } else if (stat.isFile() && fileRegex.test(f)) { 72 | return [f]; 73 | } else { 74 | return []; 75 | } 76 | }) 77 | .reduce((current, next) => current.concat(next), []); 78 | } 79 | -------------------------------------------------------------------------------- /modules/testing/builder/projects/hello-world-app/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "app": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular/build:application", 19 | "options": { 20 | "outputPath": "dist", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": ["zone.js"], 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": ["src/favicon.ico", "src/assets"], 27 | "styles": ["src/styles.scss"], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "budgets": [ 33 | { 34 | "type": "initial", 35 | "maximumWarning": "500kb", 36 | "maximumError": "1mb" 37 | }, 38 | { 39 | "type": "anyComponentStyle", 40 | "maximumWarning": "2kb", 41 | "maximumError": "4kb" 42 | } 43 | ], 44 | "outputHashing": "all" 45 | }, 46 | "development": { 47 | "optimization": false, 48 | "extractLicenses": false, 49 | "sourceMap": true 50 | } 51 | }, 52 | "defaultConfiguration": "production" 53 | }, 54 | "serve": { 55 | "builder": "@angular/build:dev-server", 56 | "configurations": { 57 | "production": { 58 | "buildTarget": "app:build:production" 59 | }, 60 | "development": { 61 | "buildTarget": "app:build:development" 62 | } 63 | }, 64 | "defaultConfiguration": "development" 65 | }, 66 | "extract-i18n": { 67 | "builder": "@angular-devkit/build-angular:extract-i18n", 68 | "options": { 69 | "buildTarget": "app:build" 70 | } 71 | }, 72 | "test": { 73 | "builder": "@angular-devkit/build-angular:karma", 74 | "options": { 75 | "polyfills": ["zone.js", "zone.js/testing"], 76 | "tsConfig": "tsconfig.spec.json", 77 | "inlineStyleLanguage": "scss", 78 | "assets": ["src/favicon.ico", "src/assets"], 79 | "styles": ["src/styles.scss"], 80 | "scripts": [] 81 | } 82 | }, 83 | "ngsscbuild": { 84 | "builder": "angular-server-side-configuration:ngsscbuild", 85 | "options": { 86 | "additionalEnvironmentVariables": [], 87 | "buildTarget": "app:build" 88 | }, 89 | "configurations": { 90 | "production": { 91 | "buildTarget": "app:build:production" 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/ngsscbuild/index.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Ngssc } from 'angular-server-side-configuration'; 2 | 3 | import { describeBuilder } from '../../../../modules/testing/builder/src'; 4 | import { Schema } from './schema'; 5 | import { ngsscBuild } from './index'; 6 | 7 | const APPLICATION_BUILDER_INFO = Object.freeze({ 8 | name: 'angular-server-side-configuration:ngsscbuild', 9 | schemaPath: __dirname + '/schema.json', 10 | }); 11 | 12 | const BASE_OPTIONS = Object.freeze({ 13 | buildTarget: 'app:build', 14 | browserTarget: 'app:build', 15 | additionalEnvironmentVariables: [], 16 | filePattern: 'index.html', 17 | }); 18 | 19 | // Disabled, as the output is currently seemingly fixed to 20 | // workspace root. 21 | describeBuilder(ngsscBuild, APPLICATION_BUILDER_INFO, (harness) => { 22 | xit('should build with process variant', async () => { 23 | harness.useTarget('ngsscbuild', { 24 | ...BASE_OPTIONS, 25 | }); 26 | 27 | const { result } = await harness.executeOnce(); 28 | 29 | expect(result?.success).toBe(true); 30 | const ngssc = JSON.parse(harness.readFile('dist/browser/ngssc.json')) as Ngssc; 31 | expect(ngssc.variant).toEqual('process'); 32 | expect(ngssc.filePattern).toEqual('index.html'); 33 | }); 34 | 35 | xit('should aggregate environment variables', async () => { 36 | const expected = 'OTHER_VARIABLE'; 37 | harness.modifyFile('angular.json', (content) => 38 | content.replace( 39 | '"additionalEnvironmentVariables": [],', 40 | `"additionalEnvironmentVariables": ["${expected}"],`, 41 | ), 42 | ); 43 | harness.useTarget('ngsscbuild', { 44 | ...BASE_OPTIONS, 45 | }); 46 | 47 | const { result } = await harness.executeOnce(); 48 | 49 | expect(result?.success).toBe(true); 50 | const ngssc = JSON.parse(harness.readFile('dist/browser/ngssc.json')) as Ngssc; 51 | expect(ngssc.environmentVariables).toContain(expected); 52 | }); 53 | 54 | xit('should write ngssc into dist/browser directory without SSR', async () => { 55 | harness.modifyFile('angular.json', (content) => 56 | content.replace( 57 | /("server":\s+"src\/main.server.ts",|"prerender":\s+true,|"ssr":\s+\{\s+"entry":\s+"server.ts"\s+\})/g, 58 | '', 59 | ), 60 | ); 61 | harness.useTarget('ngsscbuild', { 62 | ...BASE_OPTIONS, 63 | }); 64 | 65 | const { result } = await harness.executeOnce(); 66 | 67 | expect(result?.success).toBe(true); 68 | const ngssc = JSON.parse(harness.readFile('dist/browser/ngssc.json')) as Ngssc; 69 | expect(ngssc.variant).toEqual('process'); 70 | expect(ngssc.filePattern).toEqual('index.html'); 71 | }); 72 | 73 | xit('should handle object outputPath', async () => { 74 | harness.modifyFile('angular.json', (content) => 75 | content.replace( 76 | '"outputPath": "dist",', 77 | `"outputPath": { "base": "dist", "browser": "html" },`, 78 | ), 79 | ); 80 | harness.useTarget('ngsscbuild', { 81 | ...BASE_OPTIONS, 82 | }); 83 | 84 | const { result } = await harness.executeOnce(); 85 | 86 | expect(result?.success).toBe(true); 87 | const ngssc = JSON.parse(harness.readFile('dist/browser/ngssc.json')) as Ngssc; 88 | expect(ngssc.variant).toEqual('process'); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/ngsscbuild/variable-detector.spec.ts: -------------------------------------------------------------------------------- 1 | import { VariableDetector } from './variable-detector'; 2 | 3 | describe('VariableDetector', () => { 4 | const expectedEnvVariables = [ 5 | 'API_BACKEND', 6 | 'INDEX_ACCESS', 7 | 'INDEX_ACCESS2', 8 | 'NUMBER', 9 | 'OMG', 10 | 'PROD', 11 | 'SIMPLE_VALUE', 12 | 'TERNARY', 13 | ]; 14 | 15 | it('should detect process.env variables', () => { 16 | const detector = new VariableDetector(); 17 | 18 | const result = detector.detect(envContent); 19 | expect(result.variant).toBe('process'); 20 | expect(result.variables.length).toBe(8); 21 | expect(result.variables).toEqual(expectedEnvVariables); 22 | }); 23 | 24 | it('should detect NG_ENV variables with ng-env', () => { 25 | const detector = new VariableDetector(); 26 | const result = detector.detect(envContentNgEnv); 27 | expect(result.variant).toBe('NG_ENV'); 28 | expect(result.variables.length).toBe(8); 29 | expect(result.variables).toEqual(expectedEnvVariables); 30 | }); 31 | }); 32 | 33 | const envContentNgEnv = ` 34 | import { NG_ENV } from 'angular-server-side-configuration/ng-env'; 35 | 36 | /** 37 | * How to use angular-server-side-configuration: 38 | * 39 | * Use NG_ENV.NAME_OF_YOUR_ENVIRONMENT_VARIABLE 40 | * 41 | * export const environment = { 42 | * stringValue: NG_ENV.STRING_VALUE, 43 | * stringValueWithDefault: NG_ENV.STRING_VALUE || 'defaultValue', 44 | * numberValue: Number(NG_ENV.NUMBER_VALUE), 45 | * numberValueWithDefault: Number(NG_ENV.NUMBER_VALUE || 10), 46 | * booleanValue: Boolean(NG_ENV.BOOLEAN_VALUE), 47 | * booleanValueInverted: NG_ENV.BOOLEAN_VALUE_INVERTED !== 'false', 48 | * }; 49 | */ 50 | 51 | export const environment = { 52 | production: NG_ENV.PROD !== 'false', 53 | apiBackend: NG_ENV.API_BACKEND || 'http://example.com', 54 | ternary: NG_ENV.TERNARY ? 'asdf' : 'qwer', 55 | simpleValue: NG_ENV.SIMPLE_VALUE, 56 | something: { 57 | asdf: NG_ENV.OMG || 'omg', 58 | qwer: parseInt(NG_ENV.NUMBER || ''), 59 | }, 60 | indexAccess: NG_ENV['INDEX_ACCESS'], 61 | indexAccess2: NG_ENV[\`INDEX_ACCESS2\`], 62 | }; 63 | `; 64 | 65 | const envContent = ` 66 | import 'angular-server-side-configuration/process'; 67 | 68 | /** 69 | * How to use angular-server-side-configuration: 70 | * 71 | * Use process.env.NAME_OF_YOUR_ENVIRONMENT_VARIABLE 72 | * 73 | * export const environment = { 74 | * stringValue: process.env.STRING_VALUE, 75 | * stringValueWithDefault: process.env.STRING_VALUE || 'defaultValue', 76 | * numberValue: Number(process.env.NUMBER_VALUE), 77 | * numberValueWithDefault: Number(process.env.NUMBER_VALUE || 10), 78 | * booleanValue: Boolean(process.env.BOOLEAN_VALUE), 79 | * booleanValueInverted: process.env.BOOLEAN_VALUE_INVERTED !== 'false', 80 | * }; 81 | */ 82 | 83 | export const environment = { 84 | production: process.env.PROD !== 'false', 85 | apiBackend: process.env.API_BACKEND || 'http://example.com', 86 | ternary: process.env.TERNARY ? 'asdf' : 'qwer', 87 | simpleValue: process.env.SIMPLE_VALUE, 88 | something: { 89 | asdf: process.env.OMG || 'omg', 90 | qwer: parseInt(process.env.NUMBER || ''), 91 | }, 92 | indexAccess: process.env['INDEX_ACCESS'], 93 | indexAccess2: process.env[\`INDEX_ACCESS2\`], 94 | }; 95 | `; 96 | -------------------------------------------------------------------------------- /cli/insertion_target.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | ) 11 | 12 | // InsertionTarget represents an html file target 13 | type InsertionTarget struct { 14 | filePath string 15 | noncePlaceholder string 16 | ngsscConfig NgsscConfig 17 | } 18 | 19 | // Insert the environment variables into the targeted file 20 | func (target InsertionTarget) Insert() error { 21 | htmlBytes, err := os.ReadFile(target.filePath) 22 | if err != nil { 23 | return fmt.Errorf("failed to read %v\n%v", target.filePath, err) 24 | } 25 | 26 | nonce := "" 27 | if target.noncePlaceholder != "" { 28 | nonce = fmt.Sprintf( 29 | " nonce=\"%v\"", 30 | target.noncePlaceholder) 31 | } 32 | 33 | html := string(htmlBytes) 34 | iifeScript := fmt.Sprintf( 35 | "%v", 36 | nonce, 37 | target.ngsscConfig.BuildIifeScriptContent()) 38 | var newHTML string 39 | ngsscRegex := regexp.MustCompile(`[\w\W]*`) 40 | configRegex := regexp.MustCompile(``) 41 | if ngsscRegex.Match(htmlBytes) { 42 | newHTML = ngsscRegex.ReplaceAllString(html, iifeScript) 43 | } else if configRegex.Match(htmlBytes) { 44 | newHTML = configRegex.ReplaceAllString(html, iifeScript) 45 | } else if strings.Contains(html, "") { 46 | newHTML = strings.Replace(html, "", ""+iifeScript, 1) 47 | } else { 48 | newHTML = strings.Replace(html, "", iifeScript+"", 1) 49 | } 50 | 51 | newHTMLBytes := []byte(newHTML) 52 | err = os.WriteFile(target.filePath, newHTMLBytes, 0644) 53 | if err != nil { 54 | return fmt.Errorf("failed to update %v\n%v", target.filePath, err) 55 | } 56 | 57 | replaceIndexHashInNgsw(target, htmlBytes, newHTMLBytes) 58 | 59 | return nil 60 | } 61 | 62 | func replaceIndexHashInNgsw(target InsertionTarget, originalHash []byte, replacedHash []byte) { 63 | filePath := filepath.Join(filepath.Dir(target.filePath), "ngsw.json") 64 | info, err := os.Stat(filePath) 65 | if os.IsNotExist(err) || info.IsDir() { 66 | return 67 | } 68 | 69 | ngswBytes, err := os.ReadFile(filePath) 70 | if err != nil { 71 | fmt.Printf("Detected ngsw.json, but failed to read it at %v\n", filePath) 72 | return 73 | } 74 | 75 | ngswContent := string(ngswBytes) 76 | wrappedHexHash := createQuotedHash(originalHash) 77 | if !strings.Contains(ngswContent, wrappedHexHash) { 78 | fmt.Printf("Detected ngsw.json, but existing hash (%v) of the index file could not be found\n", wrappedHexHash) 79 | return 80 | } 81 | 82 | replacedWrappedHexHash := createQuotedHash(replacedHash) 83 | replacedNgswContent := strings.Replace(ngswContent, wrappedHexHash, replacedWrappedHexHash, 1) 84 | err = os.WriteFile(filePath, []byte(replacedNgswContent), info.Mode()) 85 | if err != nil { 86 | fmt.Printf("Detected ngsw.json, but failed to update it at %v\n", filePath) 87 | return 88 | } 89 | 90 | fmt.Printf("Detected ngsw.json and updated index hash at %v\n", filePath) 91 | } 92 | 93 | func createQuotedHash(bytes []byte) string { 94 | hash := sha1.New() 95 | hash.Write(bytes) 96 | hashSum := hash.Sum(nil) 97 | wrappedHexHash := fmt.Sprintf(`"%x"`, hashSum) 98 | return wrappedHexHash 99 | } 100 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/ng-update/index.ts: -------------------------------------------------------------------------------- 1 | import { basename } from '@angular-devkit/core'; 2 | import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; 3 | import { updateWorkspace } from '@schematics/angular/utility/workspace'; 4 | 5 | export function updateToV15(): Rule { 6 | return (_tree: Tree, context: SchematicContext) => { 7 | return updateWorkspace((workspace) => { 8 | context.logger.info(`Removing obsolete ngsscbuild entry 'ngsscEnvironmentFile'.`); 9 | workspace.projects.forEach((project, name) => { 10 | const ngsscbuild = project.targets.get('ngsscbuild'); 11 | if (!ngsscbuild || !ngsscbuild.options) { 12 | return; 13 | } 14 | 15 | if ('ngsscEnvironmentFile' in ngsscbuild.options) { 16 | delete ngsscbuild.options['ngsscEnvironmentFile']; 17 | context.logger.info(` - Removed from ${name} ngsscbuild options`); 18 | } 19 | Object.keys(ngsscbuild.configurations || {}) 20 | .filter((c) => 'ngsscEnvironmentFile' in ngsscbuild.configurations![c]!) 21 | .forEach((c) => { 22 | delete ngsscbuild.configurations![c]!['ngsscEnvironmentFile']; 23 | context.logger.info(` - Removed from ${name} ngsscbuild configuration ${c}`); 24 | }); 25 | }); 26 | }); 27 | }; 28 | } 29 | 30 | export function updateToV17(): Rule { 31 | return (_tree: Tree, context: SchematicContext) => { 32 | return updateWorkspace((workspace) => { 33 | context.logger.info(`Renaming 'browserTarget' to 'buildTarget'.`); 34 | workspace.projects.forEach((project, name) => { 35 | const ngsscbuild = project.targets.get('ngsscbuild'); 36 | if (!ngsscbuild || !ngsscbuild.options) { 37 | return; 38 | } 39 | 40 | if ('browserTarget' in ngsscbuild.options) { 41 | ngsscbuild.options['buildTarget'] = ngsscbuild.options['browserTarget']; 42 | delete ngsscbuild.options['browserTarget']; 43 | } 44 | Object.keys(ngsscbuild.configurations || {}) 45 | .filter((c) => 'browserTarget' in ngsscbuild.configurations![c]!) 46 | .forEach((c) => { 47 | ngsscbuild.configurations![c]!['buildTarget'] = 48 | ngsscbuild.configurations![c]!['browserTarget']; 49 | delete ngsscbuild.configurations![c]!['browserTarget']; 50 | }); 51 | }); 52 | }); 53 | }; 54 | } 55 | 56 | export function dockerfile(): Rule { 57 | return (tree: Tree) => { 58 | const downloadUrlRegexes = new Map() 59 | .set( 60 | /https:\/\/github.com\/kyubisation\/angular-server-side-configuration\/releases\/download\/v((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)/, 61 | 'https://github.com/kyubisation/angular-server-side-configuration/releases/download/v', 62 | ) 63 | .set( 64 | /https:\/\/bin.sbb.ch\/artifactory\/angular-server-side-configuration\/download\/v((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)/, 65 | 'https://bin.sbb.ch/artifactory/angular-server-side-configuration/download/v', 66 | ); 67 | const version = require('../../package.json').version; 68 | tree.visit((path, entry) => { 69 | if (basename(path).indexOf('Dockerfile') >= 0 && entry) { 70 | downloadUrlRegexes.forEach((downloadUrlTemplate, downloadUrlRegex) => { 71 | if (entry.content.toString().match(downloadUrlRegex)) { 72 | const content = entry.content 73 | .toString() 74 | .replace( 75 | new RegExp(downloadUrlRegex.source, 'g'), 76 | `${downloadUrlTemplate}${version}`, 77 | ); 78 | tree.overwrite(path, content); 79 | } 80 | }); 81 | } 82 | }); 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /cli/ngssc_config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/bmatcuk/doublestar" 13 | ) 14 | 15 | // NgsscConfig corresponds to the JSON structure of ngssc.json 16 | type NgsscConfig struct { 17 | FilePath string 18 | Variant string 19 | EnvironmentVariables map[string]*string 20 | FilePattern string 21 | } 22 | 23 | type ngsscJSON struct { 24 | Variant string 25 | EnvironmentVariables []string 26 | FilePattern *string 27 | } 28 | 29 | func FindNgsscJsonConfigs(pattern string) (ngsscConfigs []NgsscConfig, err error) { 30 | files, err := doublestar.Glob(pattern) 31 | if err != nil { 32 | return nil, fmt.Errorf("unable to resolve pattern: %v\n%v", pattern, err) 33 | } else if len(files) == 0 { 34 | return nil, fmt.Errorf("no ngssc.json files found with %v", pattern) 35 | } 36 | 37 | ngsscConfigs = make([]NgsscConfig, 0) 38 | for _, ngsscFile := range files { 39 | ngsscConfig, err := readNgsscJson(ngsscFile) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | ngsscConfigs = append(ngsscConfigs, ngsscConfig) 45 | } 46 | 47 | return ngsscConfigs, nil 48 | } 49 | 50 | func NgsscJsonConfigFromPath(ngsscFile string) (ngsscConfig NgsscConfig, err error) { 51 | if !strings.HasSuffix(ngsscFile, "ngssc.json") { 52 | ngsscFile = filepath.Join(ngsscFile, "ngssc.json") 53 | } 54 | return readNgsscJson(ngsscFile) 55 | } 56 | 57 | // readNgsscJson NgsscConfig instance from a file 58 | func readNgsscJson(path string) (ngsscConfig NgsscConfig, err error) { 59 | data, err := ioutil.ReadFile(path) 60 | if err != nil { 61 | return ngsscConfig, fmt.Errorf("failed to read %v\n%v", path, err) 62 | } 63 | 64 | var ngssc *ngsscJSON 65 | err = json.Unmarshal(data, &ngssc) 66 | if err != nil { 67 | return ngsscConfig, fmt.Errorf("failed to parse %v\n%v", path, err) 68 | } else if ngssc == nil { 69 | return ngsscConfig, fmt.Errorf("invalid ngssc.json at %v (Must not be empty)", path) 70 | } else if ngssc.EnvironmentVariables == nil { 71 | return ngsscConfig, fmt.Errorf("invalid ngssc.json at %v (environmentVariables must be defined)", path) 72 | } else if ngssc.Variant != "process" && ngssc.Variant != "global" && ngssc.Variant != "NG_ENV" { 73 | return ngsscConfig, fmt.Errorf("invalid ngssc.json at %v (variant must either be process or NG_ENV)", path) 74 | } 75 | 76 | if ngssc.FilePattern == nil { 77 | filePatternDefault := "**/index.html" 78 | ngssc.FilePattern = &filePatternDefault 79 | } 80 | 81 | ngsscConfig = NgsscConfig{ 82 | FilePath: path, 83 | Variant: ngssc.Variant, 84 | EnvironmentVariables: populateEnvironmentVariables(ngssc.EnvironmentVariables), 85 | FilePattern: *ngssc.FilePattern, 86 | } 87 | 88 | return ngsscConfig, nil 89 | } 90 | 91 | func (base NgsscConfig) VariantAndVariablesMatch(other NgsscConfig) bool { 92 | return base.Variant == other.Variant && reflect.DeepEqual(base.EnvironmentVariables, other.EnvironmentVariables) 93 | } 94 | 95 | func (ngsscConfig NgsscConfig) BuildIifeScriptContent() string { 96 | jsonBytes, err := json.Marshal(ngsscConfig.EnvironmentVariables) 97 | if err != nil { 98 | fmt.Print(err) 99 | } 100 | 101 | envMapJSON := string(jsonBytes) 102 | var iife string 103 | if ngsscConfig.Variant == "NG_ENV" { 104 | iife = fmt.Sprintf("self.NG_ENV=%v", envMapJSON) 105 | } else if ngsscConfig.Variant == "global" { 106 | iife = fmt.Sprintf("Object.assign(self,%v)", envMapJSON) 107 | } else { 108 | iife = fmt.Sprintf(`self.process={"env":%v}`, envMapJSON) 109 | } 110 | 111 | return fmt.Sprintf("(function(self){%v;})(window)", iife) 112 | } 113 | 114 | func populateEnvironmentVariables(environmentVariables []string) map[string]*string { 115 | envMap := make(map[string]*string) 116 | for _, env := range environmentVariables { 117 | value, exists := os.LookupEnv(env) 118 | if exists { 119 | envMap[env] = &value 120 | } else { 121 | envMap[env] = nil 122 | } 123 | } 124 | 125 | return envMap 126 | } 127 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/src/insert.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync } from 'fs'; 2 | import { dirname, join } from 'path'; 3 | 4 | import { globToRegExp } from './glob-to-regexp'; 5 | import type { Ngssc, Variant } from './ngssc'; 6 | 7 | export function insert( 8 | options: { dryRun?: boolean; recursive?: boolean; directory?: string } = {}, 9 | ) { 10 | const directory = options.directory || process.cwd(); 11 | if (options.recursive) { 12 | walk(directory, '**/ngssc.json') 13 | .map((f) => dirname(f)) 14 | .forEach((d) => insertWithNgssc(d, !!options.dryRun)); 15 | } else { 16 | insertWithNgssc(directory, !!options.dryRun); 17 | } 18 | } 19 | 20 | function insertWithNgssc(directory: string, dryRun: boolean) { 21 | const ngsscPath = join(directory, 'ngssc.json'); 22 | if (!existsSync(ngsscPath)) { 23 | throw new Error(`${ngsscPath} does not exist!`); 24 | } 25 | 26 | const ngssc: Ngssc = JSON.parse(readFileSync(ngsscPath, 'utf8')); 27 | const populatedVariables = populateVariables(ngssc.environmentVariables); 28 | log(`Populated environment variables (Variant: ${ngssc.variant}, ${ngsscPath})`); 29 | Object.keys(populatedVariables).forEach((k) => ` ${k}: ${populatedVariables[k]}`); 30 | const iife = generateIife(ngssc.variant, populatedVariables); 31 | const htmlPattern = ngssc.filePattern || 'index.html'; 32 | const htmlFiles = walk(directory, htmlPattern); 33 | if (!htmlFiles.length) { 34 | log(`No files found with pattern ${htmlPattern} in ${directory}`); 35 | return; 36 | } 37 | 38 | log(`Configuration will be inserted into ${htmlFiles.join(', ')}`); 39 | if (dryRun) { 40 | log('Dry run. Nothing will be inserted.'); 41 | } else { 42 | htmlFiles.forEach((f) => insertIntoHtml(f, iife)); 43 | } 44 | } 45 | 46 | function populateVariables(variables: string[]) { 47 | const populatedVariables: { [key: string]: string | null } = {}; 48 | variables.forEach( 49 | (v) => (populatedVariables[v] = v in process.env ? process.env[v] || '' : null), 50 | ); 51 | return populatedVariables; 52 | } 53 | 54 | function generateIife(variant: Variant, populatedVariables: { [key: string]: string | null }) { 55 | let iife: string; 56 | if (variant === 'NG_ENV') { 57 | iife = `(function(self){self.NG_ENV=${JSON.stringify(populatedVariables)};})(window)`; 58 | } else if (variant === 'global') { 59 | iife = `(function(self){Object.assign(self,${JSON.stringify(populatedVariables)});})(window)`; 60 | } else { 61 | iife = `(function(self){self.process=${JSON.stringify({ env: populatedVariables })};})(window)`; 62 | } 63 | return ``; 64 | } 65 | 66 | function insertIntoHtml(file: string, iife: string) { 67 | const fileContent = readFileSync(file, 'utf8'); 68 | if (/[\w\W]*/.test(fileContent)) { 69 | writeFileSync(file, fileContent.replace(/[\w\W]*/, iife), 'utf8'); 70 | } else if (//.test(fileContent)) { 71 | writeFileSync(file, fileContent.replace(//, iife), 'utf8'); 72 | } else if (fileContent.includes('')) { 73 | writeFileSync(file, fileContent.replace('', `${iife}`), 'utf8'); 74 | } else { 75 | writeFileSync(file, fileContent.replace('', `${iife}`), 'utf8'); 76 | } 77 | } 78 | 79 | function walk(root: string, filePattern: string): string[] { 80 | const fileRegex = globToRegExp(filePattern, { extended: true, globstar: true, flags: 'ig' }); 81 | const directory = root.replace(/\\/g, '/'); 82 | return readdirSync(directory) 83 | .map((f) => `${directory}/${f}`) 84 | .map((f) => { 85 | const stat = lstatSync(f); 86 | if (stat.isDirectory()) { 87 | return walk(f, filePattern); 88 | } else if (stat.isFile() && fileRegex.test(f)) { 89 | return [f]; 90 | } else { 91 | return []; 92 | } 93 | }) 94 | .reduce((current, next) => current.concat(next), []); 95 | } 96 | 97 | function log(message: string) { 98 | // tslint:disable-next-line: no-console 99 | console.log(message); 100 | } 101 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/ng-update/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; 3 | import { Schema as ApplicationOptions, Style } from '@schematics/angular/application/schema'; 4 | import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema'; 5 | import { updateWorkspace } from '@schematics/angular/utility/workspace'; 6 | 7 | const workspaceOptions: WorkspaceOptions = { 8 | name: 'workspace', 9 | newProjectRoot: 'projects', 10 | version: '16.0.0', 11 | }; 12 | 13 | const appOptions: ApplicationOptions = { 14 | inlineStyle: false, 15 | inlineTemplate: false, 16 | name: 'dummy', 17 | routing: false, 18 | skipPackageJson: false, 19 | skipTests: false, 20 | style: Style.Css, 21 | }; 22 | 23 | describe('ng-update', () => { 24 | const collectionPath = join(__dirname, '../migration.json'); 25 | let runner: SchematicTestRunner; 26 | let appTree: UnitTestTree; 27 | 28 | beforeEach(async () => { 29 | runner = new SchematicTestRunner('migrations', collectionPath); 30 | appTree = await runner.runExternalSchematic( 31 | '@schematics/angular', 32 | 'workspace', 33 | workspaceOptions, 34 | ); 35 | appTree = await runner.runExternalSchematic( 36 | '@schematics/angular', 37 | 'application', 38 | appOptions, 39 | appTree, 40 | ); 41 | }); 42 | 43 | it('should remove ngsscEnvironmentFile on v15 update', async () => { 44 | runner.registerCollection('schematics', join(__dirname, '../collection.json')); 45 | const tree = await runner.runExternalSchematic( 46 | 'schematics', 47 | 'ng-add', 48 | { project: appOptions.name }, 49 | appTree, 50 | ); 51 | await updateWorkspace((workspace) => { 52 | workspace.projects.get(appOptions.name)!.targets.get('ngsscbuild')!.options![ 53 | 'ngsscEnvironmentFile' 54 | ] = 'projects/dummy/src/environments/environment.prod.ts'; 55 | })(tree, undefined as any); 56 | const migratedTree = await runner.runSchematic('migration-v15', {}, tree); 57 | const angularJson = JSON.parse(migratedTree.readContent('angular.json')); 58 | expect( 59 | 'ngsscEnvironmentFile' in angularJson.projects[appOptions.name].architect.ngsscbuild.options, 60 | ).toBeFalse(); 61 | }); 62 | 63 | it('should remove ngsscEnvironmentFile on v15 update', async () => { 64 | runner.registerCollection('schematics', join(__dirname, '../collection.json')); 65 | const tree = await runner.runExternalSchematic( 66 | 'schematics', 67 | 'ng-add', 68 | { project: appOptions.name }, 69 | appTree, 70 | ); 71 | await updateWorkspace((workspace) => { 72 | const options = workspace.projects.get(appOptions.name)!.targets.get('ngsscbuild')!.options!; 73 | options['browserTarget'] = options['buildTarget']; 74 | delete options['buildTarget']; 75 | })(tree, undefined as any); 76 | const migratedTree = await runner.runSchematic('migration-v17', {}, tree); 77 | const angularJson = JSON.parse(migratedTree.readContent('angular.json')); 78 | expect( 79 | 'browserTarget' in angularJson.projects[appOptions.name].architect.ngsscbuild.options, 80 | ).toBeFalse(); 81 | expect( 82 | angularJson.projects[appOptions.name].architect.ngsscbuild.options['buildTarget'], 83 | ).toEqual('dummy:build'); 84 | }); 85 | 86 | it('should update Dockerfile', async () => { 87 | appTree.create( 88 | 'Dockerfile', 89 | ` 90 | FROM nginx:alpine 91 | ADD https://github.com/kyubisation/angular-server-side-configuration/releases/download/v9.0.1/ngssc_64bit /usr/sbin/ngssc 92 | RUN chmod +x /usr/sbin/ngssc 93 | COPY dist /usr/share/nginx/html 94 | COPY start.sh start.sh 95 | RUN chmod +x ./start.sh 96 | CMD ["./start.sh"] 97 | `, 98 | ); 99 | const tree = await runner.runSchematic('dockerfile', {}, appTree); 100 | 101 | const dockerfileContent = tree.read('Dockerfile')!.toString(); 102 | const version = require('../../package.json').version; 103 | expect(dockerfileContent).toContain( 104 | `https://github.com/kyubisation/angular-server-side-configuration/releases/download/v${version}/ngssc_64bit`, 105 | ); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/src/glob-to-regexp.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/fitzgen/glob-to-regexp/blob/master/index.js 2 | 3 | export interface Options { 4 | extended?: boolean | undefined; 5 | globstar?: boolean | undefined; 6 | flags?: string | undefined; 7 | } 8 | 9 | export function globToRegExp(glob: string, opts?: Options) { 10 | if (typeof glob !== 'string') { 11 | throw new TypeError('Expected a string'); 12 | } 13 | 14 | var str = String(glob); 15 | 16 | // The regexp we are building, as a string. 17 | var reStr = ''; 18 | 19 | // Whether we are matching so called "extended" globs (like bash) and should 20 | // support single character matching, matching ranges of characters, group 21 | // matching, etc. 22 | var extended = opts ? !!opts.extended : false; 23 | 24 | // When globstar is _false_ (default), '/foo/*' is translated a regexp like 25 | // '^\/foo\/.*$' which will match any string beginning with '/foo/' 26 | // When globstar is _true_, '/foo/*' is translated to regexp like 27 | // '^\/foo\/[^/]*$' which will match any string beginning with '/foo/' BUT 28 | // which does not have a '/' to the right of it. 29 | // E.g. with '/foo/*' these will match: '/foo/bar', '/foo/bar.txt' but 30 | // these will not '/foo/bar/baz', '/foo/bar/baz.txt' 31 | // Lastely, when globstar is _true_, '/foo/**' is equivelant to '/foo/*' when 32 | // globstar is _false_ 33 | var globstar = opts ? !!opts.globstar : false; 34 | 35 | // If we are doing extended matching, this boolean is true when we are inside 36 | // a group (eg {*.html,*.js}), and false otherwise. 37 | var inGroup = false; 38 | 39 | // RegExp flags (eg "i" ) to pass in to RegExp constructor. 40 | var flags = opts && typeof opts.flags === 'string' ? opts.flags : ''; 41 | 42 | var c; 43 | for (var i = 0, len = str.length; i < len; i++) { 44 | c = str[i]; 45 | 46 | switch (c) { 47 | case '/': 48 | case '$': 49 | case '^': 50 | case '+': 51 | case '.': 52 | case '(': 53 | case ')': 54 | case '=': 55 | case '!': 56 | case '|': 57 | reStr += '\\' + c; 58 | break; 59 | 60 | // @ts-ignore 61 | case '?': 62 | if (extended) { 63 | reStr += '.'; 64 | break; 65 | } 66 | 67 | case '[': 68 | // @ts-ignore 69 | case ']': 70 | if (extended) { 71 | reStr += c; 72 | break; 73 | } 74 | 75 | // @ts-ignore 76 | case '{': 77 | if (extended) { 78 | inGroup = true; 79 | reStr += '('; 80 | break; 81 | } 82 | 83 | // @ts-ignore 84 | case '}': 85 | if (extended) { 86 | inGroup = false; 87 | reStr += ')'; 88 | break; 89 | } 90 | 91 | case ',': 92 | if (inGroup) { 93 | reStr += '|'; 94 | break; 95 | } 96 | reStr += '\\' + c; 97 | break; 98 | 99 | case '*': 100 | // Move over all consecutive "*"'s. 101 | // Also store the previous and next characters 102 | var prevChar = str[i - 1]; 103 | var starCount = 1; 104 | while (str[i + 1] === '*') { 105 | starCount++; 106 | i++; 107 | } 108 | var nextChar = str[i + 1]; 109 | 110 | if (!globstar) { 111 | // globstar is disabled, so treat any number of "*" as one 112 | reStr += '.*'; 113 | } else { 114 | // globstar is enabled, so determine if this is a globstar segment 115 | var isGlobstar = 116 | starCount > 1 && // multiple "*"'s 117 | (prevChar === '/' || prevChar === undefined) && // from the start of the segment 118 | (nextChar === '/' || nextChar === undefined); // to the end of the segment 119 | 120 | if (isGlobstar) { 121 | // it's a globstar, so match zero or more path segments 122 | reStr += '((?:[^/]*(?:/|$))*)'; 123 | i++; // move over the "/" 124 | } else { 125 | // it's not a globstar, so only match one path segment 126 | reStr += '([^/]*)'; 127 | } 128 | } 129 | break; 130 | 131 | default: 132 | reStr += c; 133 | } 134 | } 135 | 136 | // When regexp 'g' flag is specified don't 137 | // constrain the regular expression with ^ & $ 138 | if (!flags || !~flags.indexOf('g')) { 139 | reStr = '^' + reStr + '$'; 140 | } 141 | 142 | return new RegExp(reStr, flags); 143 | } 144 | -------------------------------------------------------------------------------- /cli/helpers_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | type TestResult struct { 15 | stdout string 16 | err error 17 | } 18 | 19 | func runWithArgs(additionalArgs ...string) TestResult { 20 | args := os.Args[0:1] 21 | args = append(args, additionalArgs...) 22 | 23 | old := os.Stdout // keep backup of the real stdout 24 | r, w, _ := os.Pipe() 25 | os.Stdout = w 26 | 27 | err := run(args) 28 | 29 | outC := make(chan string) 30 | // copy the output in a separate goroutine so printing can't block indefinitely 31 | go func() { 32 | var buf bytes.Buffer 33 | io.Copy(&buf, r) 34 | outC <- buf.String() 35 | }() 36 | 37 | // back to normal state 38 | w.Close() 39 | os.Stdout = old // restoring the real stdout 40 | 41 | return TestResult{ 42 | stdout: <-outC, 43 | err: err, 44 | } 45 | } 46 | 47 | func (result TestResult) Success() bool { 48 | return result.err == nil 49 | } 50 | 51 | func (result TestResult) Error() error { 52 | return result.err 53 | } 54 | 55 | func (result TestResult) Stdout() string { 56 | return result.stdout 57 | } 58 | 59 | func (result TestResult) StdoutContains(pattern string) bool { 60 | return strings.Contains(result.Stdout(), pattern) 61 | } 62 | 63 | type TestDir struct { 64 | path string 65 | } 66 | 67 | func newTestDir(t *testing.T) TestDir { 68 | dir := t.TempDir() 69 | return TestDir{path: dir} 70 | } 71 | 72 | func (context TestDir) CreateFile(fileName string, content string) { 73 | filePath := filepath.Join(context.path, fileName) 74 | ioutil.WriteFile(filePath, []byte(content), 0644) 75 | } 76 | 77 | func (context TestDir) FileContains(fileName string, pattern string) bool { 78 | return strings.Contains(context.ReadFile(fileName), pattern) 79 | } 80 | 81 | func (context TestDir) ReadFile(fileName string) string { 82 | filePath := filepath.Join(context.path, fileName) 83 | fileContent, err := ioutil.ReadFile(filePath) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | return string(fileContent) 89 | } 90 | 91 | func (context TestDir) CreateDirectory(language string) TestDir { 92 | path := filepath.Join(context.path, language) 93 | err := os.Mkdir(path, 0755) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | return TestDir{path} 99 | } 100 | 101 | func chdir(t *testing.T, dir string) { 102 | t.Helper() 103 | wd, err := os.Getwd() 104 | if err != nil { 105 | t.Fatalf("chdir %s: %v", dir, err) 106 | } 107 | if err := os.Chdir(dir); err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | t.Cleanup(func() { 112 | if err := os.Chdir(wd); err != nil { 113 | t.Fatalf("restoring working directory: %v", err) 114 | } 115 | }) 116 | } 117 | 118 | func tmpEnv(t *testing.T, key string, value string) { 119 | os.Setenv("TEST_VALUE", "example value") 120 | 121 | t.Cleanup(func() { 122 | os.Unsetenv("TEST_VALUE") 123 | }) 124 | } 125 | 126 | func assertContains(t *testing.T, s string, substring string, message string) { 127 | t.Helper() 128 | debugMessage := fmt.Sprintf("Expected %v to contain %v", s, substring) 129 | assertTrue(t, strings.Contains(s, substring), appendDebugMessage(message, debugMessage)) 130 | } 131 | 132 | func assertNotContains(t *testing.T, s string, substring string, message string) { 133 | t.Helper() 134 | debugMessage := fmt.Sprintf("Expected %v to not contain %v", s, substring) 135 | assertTrue(t, !strings.Contains(s, substring), appendDebugMessage(message, debugMessage)) 136 | } 137 | 138 | func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { 139 | t.Helper() 140 | assertTrue(t, a == b, appendDebugMessage(message, fmt.Sprintf("%v != %v", a, b))) 141 | } 142 | 143 | func assertFailure(t *testing.T, result TestResult) { 144 | t.Helper() 145 | if !result.Success() { 146 | return 147 | } 148 | t.Fatalf("Expected to fail, but succeeded: %v %v", result.Stdout(), result.Error()) 149 | } 150 | 151 | func assertSuccess(t *testing.T, result TestResult) { 152 | t.Helper() 153 | if result.Success() { 154 | return 155 | } 156 | t.Fatalf("Expected to succeed, but failed: %v %v", result.Stdout(), result.Error()) 157 | } 158 | 159 | func assertTrue(t *testing.T, v bool, message string) { 160 | t.Helper() 161 | if v { 162 | return 163 | } 164 | if len(message) == 0 { 165 | message = "Expected value to be true" 166 | } 167 | t.Fatal(message) 168 | } 169 | 170 | func appendDebugMessage(message string, debugMessage string) string { 171 | if len(message) == 0 { 172 | return debugMessage 173 | } else { 174 | return message + "\n" + debugMessage 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/ng-add/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; 3 | import { Schema as ApplicationOptions, Style } from '@schematics/angular/application/schema'; 4 | import { getWorkspace } from '@schematics/angular/utility/workspace'; 5 | import { Schema as WorkspaceOptions } from '@schematics/angular/workspace/schema'; 6 | 7 | const workspaceOptions: WorkspaceOptions = { 8 | name: 'workspace', 9 | newProjectRoot: 'projects', 10 | version: '20.0.0', 11 | }; 12 | 13 | const appOptions: ApplicationOptions = { 14 | inlineStyle: false, 15 | inlineTemplate: false, 16 | name: 'dummy', 17 | routing: false, 18 | skipPackageJson: false, 19 | skipTests: false, 20 | style: Style.Css, 21 | }; 22 | 23 | describe('ng-add', () => { 24 | const collectionPath = join(__dirname, '../collection.json'); 25 | const htmlPath = 'projects/dummy/src/index.html'; 26 | let runner: SchematicTestRunner; 27 | let appTree: UnitTestTree; 28 | 29 | beforeEach(async () => { 30 | runner = new SchematicTestRunner('schematics', collectionPath); 31 | appTree = await runner.runExternalSchematic( 32 | '@schematics/angular', 33 | 'workspace', 34 | workspaceOptions, 35 | ); 36 | appTree = await runner.runExternalSchematic( 37 | '@schematics/angular', 38 | 'application', 39 | appOptions, 40 | appTree, 41 | ); 42 | }); 43 | 44 | async function assertAppliedConfig( 45 | tree: UnitTestTree, 46 | importString = `import 'angular-server-side-configuration/process';`, 47 | ) { 48 | const workspace = await getWorkspace(tree); 49 | expect(workspace.projects.get(appOptions.name)!.targets.get('ngsscbuild')!.builder).toBe( 50 | 'angular-server-side-configuration:ngsscbuild', 51 | ); 52 | 53 | const environmentContent = tree.readContent('projects/dummy/src/app/app.config.ts'); 54 | expect(environmentContent).toContain(importString); 55 | 56 | const indexContent = tree.readContent(htmlPath); 57 | expect(indexContent).toContain(''); 58 | 59 | const packageJson = JSON.parse(tree.readContent('package.json')); 60 | expect(packageJson.scripts['build:ngssc']).toContain(':ngsscbuild:production'); 61 | } 62 | 63 | it('should fail with missing package.json', async () => { 64 | appTree.delete('package.json'); 65 | try { 66 | await runner.runSchematic('ng-add', { project: appOptions.name }, appTree); 67 | fail(); 68 | // tslint:disable-next-line: no-empty 69 | } catch {} 70 | }); 71 | 72 | it('should fail with missing index.html', async () => { 73 | appTree.delete(htmlPath); 74 | try { 75 | await runner.runSchematic('ng-add', { project: appOptions.name }, appTree); 76 | fail(); 77 | // tslint:disable-next-line: no-empty 78 | } catch {} 79 | }); 80 | 81 | it('should add ngssc content to correct files', async () => { 82 | const tree = await runner.runSchematic('ng-add', { project: appOptions.name }, appTree); 83 | await assertAppliedConfig(tree); 84 | }); 85 | 86 | it('should add ngssc content to correct files and split additional environment variables', async () => { 87 | const expected = 'OTHER_VARIABLES,OTHER_VARIABLES2'; 88 | const tree = await runner.runSchematic( 89 | 'ng-add', 90 | { project: appOptions.name, additionalEnvironmentVariables: expected }, 91 | appTree, 92 | ); 93 | await assertAppliedConfig(tree); 94 | const workspace = await getWorkspace(tree); 95 | expect( 96 | JSON.stringify( 97 | workspace.projects.get(appOptions.name)!.targets.get('ngsscbuild')!.options![ 98 | 'additionalEnvironmentVariables' 99 | ], 100 | ), 101 | ).toEqual(JSON.stringify(expected.split(','))); 102 | }); 103 | 104 | it('should add ngssc content to correct files, with missing title tag', async () => { 105 | const htmlContent = appTree.readContent(htmlPath); 106 | appTree.overwrite(htmlPath, htmlContent.replace(/[^<]+<\/title>/, '')); 107 | const tree = await runner.runSchematic('ng-add', { project: appOptions.name }, appTree); 108 | await assertAppliedConfig(tree); 109 | }); 110 | 111 | it('should skip adding content when run twice', async () => { 112 | const initialTree = await runner.runSchematic('ng-add', { project: appOptions.name }, appTree); 113 | const logs: string[] = []; 114 | runner.logger.subscribe((m) => logs.push(m.message)); 115 | await runner.runSchematic('ng-add', { project: appOptions.name }, initialTree); 116 | 117 | expect(logs.filter((l) => l.includes('Skipping')).length).toBe(4); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/urfave/cli" 10 | ) 11 | 12 | // CliVersion will be injected during build 13 | var CliVersion string 14 | 15 | func main() { 16 | err := run(os.Args) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | } 21 | 22 | func run(args []string) error { 23 | ngssc := cli.NewApp() 24 | ngssc.Name = "ngssc" 25 | ngssc.Usage = "Angular Server Side Configuration" 26 | ngssc.Version = CliVersion 27 | 28 | ngssc.Before = separator 29 | ngssc.After = separator 30 | ngssc.Commands = []cli.Command{ 31 | { 32 | Before: title, 33 | Name: "insert", 34 | Usage: "Insert environment variables. Looks for an ngssc.json file inside the current or " + 35 | "given directory. Directory defaults to current working directory.", 36 | UsageText: "[WORKING_DIRECTORY]", 37 | Flags: []cli.Flag{ 38 | cli.BoolFlag{ 39 | Name: "recursive, r", 40 | Usage: "Recursively searches for ngssc.json files and applies the contained configuration.", 41 | }, 42 | cli.BoolFlag{ 43 | Name: "nginx", 44 | Usage: "Applies default configuration for ngssc insert to work with nginx. " + 45 | "Sets working directory to /usr/share/nginx/html/ and recursive to true.", 46 | }, 47 | cli.BoolFlag{ 48 | Name: "dry", 49 | Usage: "Perform the insert without actually inserting the variables.", 50 | }, 51 | cli.StringFlag{ 52 | Name: "nonce", 53 | Usage: "Generates a nonce in the script tag with the given placeholder.", 54 | }, 55 | }, 56 | Action: InsertCommand, 57 | }, 58 | { 59 | Before: title, 60 | Name: "substitute", 61 | Usage: `Substitutes the variable ${NGSSC_CSP_HASH} in files ending with ".template" and copies the file while removing the ".template" extension. 62 | 63 | ${NGSSC_CSP_HASH} represents the CSP hash value of the IIFE generated/inserted by the 64 | insert command, wrapped by single quotes. 65 | 66 | By default looks for "*.template" files in the current working directory. Specify another 67 | directory to search for "*.template" files via argument. 68 | (e.g. ngssc substitute /path/to/template/files) 69 | 70 | When applying the variable(s), the file is copied to the same directory without the 71 | ".template" extension with the substituion applied. 72 | (e.g. ngssc substitute: /a/my.conf.template => /a/my.conf) 73 | 74 | Use the "--out" flag to define a different output directory. 75 | (e.g. ngssc substitute --out=/b: /a/my.conf.template => /b/my.conf) 76 | 77 | Optionally supports substituting environment variables with the --include-env flag. 78 | The format ${EXAMPLE} must be used ($EXAMPLE will not work). Additionally only 79 | alphanumeric characters and _ are allowed as variable names (e.g. ${EXAMPLE_KEY}). 80 | (e.g. ngssc substitute --include-env) 81 | `, 82 | UsageText: "[TEMPLATE_DIRECTORY]", 83 | Flags: []cli.Flag{ 84 | cli.StringFlag{ 85 | Name: "ngssc-path", 86 | Usage: "Path to the ngssc.json file or containing directory to be used for the " + 87 | "generated IIFE. Supports glob. " + 88 | "Defaults to [current working directory]/**/ngssc.json. " + 89 | "Throws if multiple ngssc.json with different variant or variables are found.", 90 | }, 91 | cli.StringFlag{ 92 | Name: "hash-algorithm, a", 93 | Usage: "The hash algorithm to be used. Supports sha256, sha384 and sha512. " + 94 | "Defaults to sha512.", 95 | }, 96 | cli.StringFlag{ 97 | Name: "out, o", 98 | Usage: "The directory into which the updated files should be copied.", 99 | }, 100 | cli.BoolFlag{ 101 | Name: "include-env, e", 102 | Usage: "Substitute all variables in the format of ${VARIABLE_NAME}.", 103 | }, 104 | cli.BoolFlag{ 105 | Name: "nginx", 106 | Usage: "Applies default configuration for ngssc substitute to work with nginx. " + 107 | "Sets ngssc-path to /usr/share/nginx/html/, template directory to " + 108 | "/etc/nginx/ngssc-templates/ and out directory to /etc/nginx/conf.d/.", 109 | }, 110 | cli.BoolFlag{ 111 | Name: "dry", 112 | Usage: "Perform the insert without actually inserting the variables.", 113 | }, 114 | }, 115 | Action: SubstituteCommand, 116 | }, 117 | } 118 | 119 | return ngssc.Run(args) 120 | } 121 | 122 | func separator(c *cli.Context) error { 123 | fmt.Println() 124 | return nil 125 | } 126 | 127 | func title(c *cli.Context) error { 128 | title := fmt.Sprintf("%v(%v) %v", c.App.Name, CliVersion, c.Command.Name) 129 | fmt.Printf("%v\n%v\n\n", title, strings.Repeat("=", len(title))) 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-server-side-configuration", 3 | "version": "21.0.2", 4 | "description": "Configure an angular application on the server", 5 | "scripts": { 6 | "build:lib": "node ./scripts/build-lib.mts", 7 | "build:cli": "node ./scripts/build-cli.mts", 8 | "build:ngssc": "ng run ngssc-app:ngsscbuild:production", 9 | "build:demo": "ng build ngssc-app", 10 | "build:demo-i18n": "ng build ngssc-app -c i18n", 11 | "build": "run-s build:*", 12 | "format": "prettier --write **/*.{js,ts,css,scss,json,md,html}", 13 | "pretest:lib": "yarn -s build:lib", 14 | "test:lib": "tsx test/jasmine.js", 15 | "test:app": "ng test ngssc-app --configuration=ci", 16 | "test:cli": "cd cli && go test", 17 | "test:cli:coverage": "cd cli && go test -coverprofile=coverage.out && go tool cover -html=coverage.out", 18 | "test:container": "docker build --tag ngssc-test --file test/Dockerfile.test . && docker run --rm -it -p 8080:8080 ngssc-test", 19 | "test": "run-s test:lib test:app test:cli", 20 | "serve:demo": "cross-env VARIABLE=demo ng serve ngssc-builders-app", 21 | "prepack:lib": "yarn -s build:lib", 22 | "pack:lib": "cd dist/angular-server-side-configuration && yarn pack", 23 | "lint": "ng lint", 24 | "release": "commit-and-tag-version" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/kyubisation/angular-server-side-configuration.git" 29 | }, 30 | "keywords": [ 31 | "angular", 32 | "configuration", 33 | "server", 34 | "server-side", 35 | "docker", 36 | "openshift", 37 | "kubernetes" 38 | ], 39 | "author": "kyubisation", 40 | "license": "Apache-2.0", 41 | "bugs": { 42 | "url": "https://github.com/kyubisation/angular-server-side-configuration/issues" 43 | }, 44 | "homepage": "https://github.com/kyubisation/angular-server-side-configuration#readme", 45 | "private": true, 46 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", 47 | "dependencies": { 48 | "@angular/animations": "^21.0.3", 49 | "@angular/common": "^21.0.3", 50 | "@angular/compiler": "^21.0.3", 51 | "@angular/core": "^21.0.3", 52 | "@angular/forms": "^21.0.3", 53 | "@angular/platform-browser": "^21.0.3", 54 | "@angular/platform-browser-dynamic": "^21.0.3", 55 | "@angular/platform-server": "^21.0.3", 56 | "@angular/router": "^21.0.3", 57 | "@angular/ssr": "^21.0.2", 58 | "rxjs": "7.8.2", 59 | "tslib": "^2.8.1", 60 | "zone.js": "~0.15.0" 61 | }, 62 | "devDependencies": { 63 | "@angular-devkit/architect": "^0.2100.2", 64 | "@angular-devkit/core": "^21.0.2", 65 | "@angular-devkit/schematics": "^21.0.2", 66 | "@angular-eslint/builder": "21.0.1", 67 | "@angular-eslint/eslint-plugin": "21.0.1", 68 | "@angular-eslint/eslint-plugin-template": "21.0.1", 69 | "@angular-eslint/schematics": "21.0.1", 70 | "@angular-eslint/template-parser": "21.0.1", 71 | "@angular/build": "^21.0.2", 72 | "@angular/cli": "^21.0.2", 73 | "@angular/compiler-cli": "^21.0.3", 74 | "@angular/localize": "21.0.3", 75 | "@types/jasmine": "~5.1.13", 76 | "@types/node": "^24.10.1", 77 | "@typescript-eslint/eslint-plugin": "^8.33.1", 78 | "@typescript-eslint/parser": "^8.33.1", 79 | "@typescript-eslint/types": "^8.48.1", 80 | "@typescript-eslint/utils": "^8.33.1", 81 | "commit-and-tag-version": "^12.6.1", 82 | "cross-env": "^10.1.0", 83 | "eslint": "^9.28.0", 84 | "eslint-plugin-import-x": "^4.16.1", 85 | "glob": "^10.4.5", 86 | "jasmine": "~5.4.0", 87 | "jasmine-core": "~5.4.0", 88 | "karma": "~6.4.3", 89 | "karma-chrome-launcher": "~3.2.0", 90 | "karma-coverage": "~2.2.1", 91 | "karma-jasmine": "~5.1.0", 92 | "karma-jasmine-html-reporter": "~2.1.0", 93 | "ng-packagr": "^21.0.0", 94 | "npm-run-all": "^4.1.5", 95 | "prettier": "3.7.4", 96 | "tsx": "^4.21.0", 97 | "typescript": "~5.9.3" 98 | }, 99 | "prettier": { 100 | "singleQuote": true, 101 | "endOfLine": "lf", 102 | "printWidth": 100 103 | }, 104 | "commit-and-tag-version": { 105 | "bumpFiles": [ 106 | { 107 | "filename": "package.json", 108 | "type": "json" 109 | }, 110 | { 111 | "filename": "projects/angular-server-side-configuration/package.json", 112 | "updater": "scripts/standard-version-updater.js" 113 | }, 114 | { 115 | "filename": "projects/angular-server-side-configuration/schematics/migration.json", 116 | "updater": "scripts/standard-version-updater.js" 117 | }, 118 | { 119 | "filename": "README.md", 120 | "updater": "scripts/standard-version-updater.js" 121 | } 122 | ] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cli/substitution_task.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "crypto/sha512" 6 | "encoding/base64" 7 | "fmt" 8 | "hash" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/bmatcuk/doublestar" 14 | ) 15 | 16 | type SubstitutionTask struct { 17 | workingDirectory string 18 | templateDirectory string 19 | ngsscPath string 20 | hashAlgorithm string 21 | out string 22 | includeEnv bool 23 | dryRun bool 24 | } 25 | 26 | func (task SubstitutionTask) Substitute() error { 27 | if _, err := os.Stat(task.templateDirectory); os.IsNotExist(err) { 28 | return fmt.Errorf("template directory does not exist:%v\n%v", task.templateDirectory, err) 29 | } 30 | 31 | fmt.Printf("Template directory:\n %v\n", task.templateDirectory) 32 | 33 | ngsscConfig, err := resolveNgsscConfig(task.ngsscPath, task.workingDirectory) 34 | if err != nil { 35 | return err 36 | } 37 | fmt.Printf("Resolved ngssc.json config:\n %v\n", ngsscConfig.FilePath) 38 | 39 | scriptHash := generateIifeScriptHash(ngsscConfig, task.hashAlgorithm) 40 | substitutionFiles, err := resolveSubstitutionFiles(task.templateDirectory) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | variableMap := createVariablesMap(scriptHash, task.includeEnv) 46 | outDir := task.out 47 | if outDir != "" && !filepath.IsAbs(outDir) { 48 | outDir = filepath.Join(task.workingDirectory, outDir) 49 | } 50 | 51 | if outDir != "" && !task.dryRun { 52 | err := os.MkdirAll(outDir, os.ModePerm) 53 | if err != nil { 54 | return fmt.Errorf("failed to create directory %v\n%v", outDir, err) 55 | } 56 | } 57 | 58 | return substituteVariables(substitutionFiles, outDir, variableMap, task.dryRun) 59 | } 60 | 61 | func generateIifeScriptHash(ngsscConfig NgsscConfig, hashAlgorithmString string) string { 62 | hashAlgorithm, hashName := resolveHashAlgorithm(hashAlgorithmString) 63 | hashAlgorithm.Write([]byte(ngsscConfig.BuildIifeScriptContent())) 64 | hashSum := hashAlgorithm.Sum(nil) 65 | hashBase64 := base64.StdEncoding.EncodeToString(hashSum) 66 | hashResult := fmt.Sprintf(`'%v-%v'`, hashName, hashBase64) 67 | return hashResult 68 | } 69 | 70 | func resolveHashAlgorithm(hashAlgorithmString string) (hash.Hash, string) { 71 | hashAlgorithm := strings.ToLower(hashAlgorithmString) 72 | if hashAlgorithm == "" || hashAlgorithm == "sha512" { 73 | return sha512.New(), "sha512" 74 | } else if hashAlgorithm == "sha384" { 75 | return sha512.New384(), "sha384" 76 | } else if hashAlgorithm == "sha256" { 77 | return sha256.New(), "sha256" 78 | } else { 79 | fmt.Printf("Unknown hash algorithm %v. Using sha512 instead.", hashAlgorithmString) 80 | return sha512.New(), "sha512" 81 | } 82 | } 83 | 84 | func resolveNgsscConfig(ngsscPath string, workingDirectory string) (ngsscConfig NgsscConfig, err error) { 85 | if ngsscPath == "" { 86 | ngsscPath = filepath.Join(workingDirectory, "**", "ngssc.json") 87 | } else if filepath.Base(ngsscPath) != "ngssc.json" { 88 | ngsscPath = filepath.Join(ngsscPath, "ngssc.json") 89 | } 90 | 91 | configs, err := FindNgsscJsonConfigs(ngsscPath) 92 | if err != nil { 93 | return NgsscConfig{}, err 94 | } else if len(configs) == 1 { 95 | return configs[0], nil 96 | } 97 | 98 | pivot := configs[0] 99 | for _, ngsscConfig := range configs { 100 | if !pivot.VariantAndVariablesMatch(ngsscConfig) { 101 | return NgsscConfig{}, 102 | fmt.Errorf( 103 | "all recursively found ngssc.json must have same variant and environment variables configuration. (See %v and %v)", 104 | pivot.FilePath, 105 | ngsscConfig.FilePath) 106 | } 107 | } 108 | 109 | return pivot, nil 110 | } 111 | 112 | func resolveSubstitutionFiles(templateDirectory string) (resolvedFiles []string, err error) { 113 | pattern := filepath.Join(templateDirectory, "*.template") 114 | globFiles, err := doublestar.Glob(pattern) 115 | if err != nil { 116 | return nil, fmt.Errorf("unable to resolve pattern: %v\n%v", pattern, err) 117 | } else if len(globFiles) == 0 { 118 | return nil, fmt.Errorf("no files found with %v", pattern) 119 | } 120 | 121 | return globFiles, nil 122 | } 123 | 124 | func createVariablesMap(scriptHash string, includeEnv bool) map[string]*string { 125 | envMap := make(map[string]*string) 126 | if includeEnv { 127 | for _, e := range os.Environ() { 128 | pair := strings.SplitN(e, "=", 2) 129 | envMap[pair[0]] = &pair[1] 130 | } 131 | } 132 | 133 | envMap["NGSSC_CSP_HASH"] = &scriptHash 134 | 135 | return envMap 136 | } 137 | 138 | func substituteVariables(substitutionFiles []string, outDir string, envMap map[string]*string, dryRun bool) error { 139 | for _, templateFile := range substitutionFiles { 140 | substituteTarget := createSubstitutionTarget(templateFile, outDir, envMap, dryRun) 141 | err := substituteTarget.Substitute() 142 | if err != nil { 143 | return err 144 | } 145 | } 146 | 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /modules/testing/builder/src/builder-harness_spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.dev/license 7 | */ 8 | 9 | /* eslint-disable import/no-extraneous-dependencies */ 10 | 11 | import { TestProjectHost } from '@angular-devkit/architect/testing'; 12 | import { BuilderHarness } from './builder-harness'; 13 | 14 | describe('BuilderHarness', () => { 15 | let mockHost: TestProjectHost; 16 | 17 | beforeEach(() => { 18 | mockHost = jasmine.createSpyObj('TestProjectHost', ['root']); 19 | (mockHost.root as jasmine.Spy).and.returnValue('.'); 20 | }); 21 | 22 | it('uses the provided builder handler', async () => { 23 | const mockHandler = jasmine.createSpy().and.returnValue({ success: true }); 24 | 25 | const harness = new BuilderHarness(mockHandler, mockHost); 26 | 27 | await harness.executeOnce(); 28 | 29 | expect(mockHandler).toHaveBeenCalled(); 30 | }); 31 | 32 | it('provides the builder output result when executing', async () => { 33 | const mockHandler = jasmine.createSpy().and.returnValue({ success: false, property: 'value' }); 34 | 35 | const harness = new BuilderHarness(mockHandler, mockHost); 36 | const { result } = await harness.executeOnce(); 37 | 38 | expect(result).toBeDefined(); 39 | expect(result?.success).toBeFalse(); 40 | expect(result?.property).toBe('value'); 41 | }); 42 | 43 | it('does not show builder logs on console when a builder succeeds', async () => { 44 | const consoleErrorMock = spyOn(console, 'error'); 45 | 46 | const harness = new BuilderHarness(async (_, context) => { 47 | context.logger.warn('TEST WARNING'); 48 | 49 | return { success: true }; 50 | }, mockHost); 51 | 52 | const { result } = await harness.executeOnce(); 53 | 54 | expect(result).toBeDefined(); 55 | expect(result?.success).toBeTrue(); 56 | 57 | expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING')); 58 | }); 59 | 60 | it('shows builder logs on console when a builder fails', async () => { 61 | const consoleErrorMock = spyOn(console, 'error'); 62 | 63 | const harness = new BuilderHarness(async (_, context) => { 64 | context.logger.warn('TEST WARNING'); 65 | 66 | return { success: false }; 67 | }, mockHost); 68 | 69 | const { result } = await harness.executeOnce(); 70 | 71 | expect(result).toBeDefined(); 72 | expect(result?.success).toBeFalse(); 73 | 74 | expect(consoleErrorMock).toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING')); 75 | }); 76 | 77 | it('does not show builder logs on console when a builder fails and outputLogsOnFailure: false', async () => { 78 | const consoleErrorMock = spyOn(console, 'error'); 79 | 80 | const harness = new BuilderHarness(async (_, context) => { 81 | context.logger.warn('TEST WARNING'); 82 | 83 | return { success: false }; 84 | }, mockHost); 85 | 86 | const { result } = await harness.executeOnce({ outputLogsOnFailure: false }); 87 | 88 | expect(result).toBeDefined(); 89 | expect(result?.success).toBeFalse(); 90 | 91 | expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING')); 92 | }); 93 | 94 | it('provides and logs the builder output exception when builder throws', async () => { 95 | const mockHandler = jasmine.createSpy().and.throwError(new Error('Builder Error')); 96 | const consoleErrorMock = spyOn(console, 'error'); 97 | 98 | const harness = new BuilderHarness(mockHandler, mockHost); 99 | const { result, error } = await harness.executeOnce(); 100 | 101 | expect(result).toBeUndefined(); 102 | expect(error).toEqual(jasmine.objectContaining({ message: 'Builder Error' })); 103 | expect(consoleErrorMock).toHaveBeenCalledWith(jasmine.stringMatching('Builder Error')); 104 | }); 105 | 106 | it('does not log exception with outputLogsOnException false when builder throws', async () => { 107 | const mockHandler = jasmine.createSpy().and.throwError(new Error('Builder Error')); 108 | const consoleErrorMock = spyOn(console, 'error'); 109 | 110 | const harness = new BuilderHarness(mockHandler, mockHost); 111 | const { result, error } = await harness.executeOnce({ outputLogsOnException: false }); 112 | 113 | expect(result).toBeUndefined(); 114 | expect(error).toEqual(jasmine.objectContaining({ message: 'Builder Error' })); 115 | expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('Builder Error')); 116 | }); 117 | 118 | it('supports executing a target from within a builder', async () => { 119 | const mockHandler = jasmine.createSpy().and.returnValue({ success: true }); 120 | 121 | const harness = new BuilderHarness(async (_, context) => { 122 | const run = await context.scheduleTarget({ project: 'test', target: 'another' }); 123 | expect(await run.result).toEqual(jasmine.objectContaining({ success: true })); 124 | await run.stop(); 125 | 126 | return { success: true }; 127 | }, mockHost); 128 | harness.withBuilderTarget('another', mockHandler); 129 | 130 | const { result } = await harness.executeOnce(); 131 | 132 | expect(result).toBeDefined(); 133 | expect(result?.success).toBeTrue(); 134 | 135 | expect(mockHandler).toHaveBeenCalled(); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /modules/testing/builder/src/test-utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.dev/license 7 | */ 8 | 9 | /* eslint-disable import/no-extraneous-dependencies */ 10 | 11 | import { Architect, BuilderOutput, ScheduleOptions, Target } from '@angular-devkit/architect'; 12 | import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; 13 | import { TestProjectHost, TestingArchitectHost } from '@angular-devkit/architect/testing'; 14 | import { 15 | Path, 16 | getSystemPath, 17 | join, 18 | json, 19 | normalize, 20 | schema, 21 | virtualFs, 22 | workspaces, 23 | } from '@angular-devkit/core'; 24 | import path from 'node:path'; 25 | import { firstValueFrom } from 'rxjs'; 26 | 27 | // Default timeout for large specs is 60s. 28 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 60_000; 29 | 30 | export const workspaceRoot = join(normalize(__dirname), `../projects/hello-world-app/`); 31 | export const host = new TestProjectHost(workspaceRoot); 32 | export const outputPath: Path = normalize('dist'); 33 | 34 | export const browserTargetSpec = { project: 'app', target: 'build' }; 35 | export const devServerTargetSpec = { project: 'app', target: 'serve' }; 36 | export const extractI18nTargetSpec = { project: 'app', target: 'extract-i18n' }; 37 | export const karmaTargetSpec = { project: 'app', target: 'test' }; 38 | export const tslintTargetSpec = { project: 'app', target: 'lint' }; 39 | export const protractorTargetSpec = { project: 'app-e2e', target: 'e2e' }; 40 | 41 | export async function createArchitect(workspaceRoot: Path) { 42 | const registry = new schema.CoreSchemaRegistry(); 43 | registry.addPostTransform(schema.transforms.addUndefinedDefaults); 44 | const workspaceSysPath = getSystemPath(workspaceRoot); 45 | 46 | // The download path is relative (set from Starlark), so before potentially 47 | // changing directories, or executing inside a temporary directory, ensure 48 | // the path is absolute. 49 | if (process.env['PUPPETEER_DOWNLOAD_PATH']) { 50 | process.env.PUPPETEER_DOWNLOAD_PATH = path.resolve(process.env['PUPPETEER_DOWNLOAD_PATH']); 51 | } 52 | 53 | const { workspace } = await workspaces.readWorkspace( 54 | workspaceSysPath, 55 | workspaces.createWorkspaceHost(host), 56 | ); 57 | const architectHost = new TestingArchitectHost( 58 | workspaceSysPath, 59 | workspaceSysPath, 60 | new WorkspaceNodeModulesArchitectHost(workspace, workspaceSysPath), 61 | ); 62 | const architect = new Architect(architectHost, registry); 63 | 64 | return { 65 | workspace, 66 | architectHost, 67 | architect, 68 | }; 69 | } 70 | 71 | export interface BrowserBuildOutput { 72 | output: BuilderOutput; 73 | files: { [file: string]: Promise<string> }; 74 | } 75 | 76 | export async function browserBuild( 77 | architect: Architect, 78 | host: virtualFs.Host, 79 | target: Target, 80 | overrides?: json.JsonObject, 81 | scheduleOptions?: ScheduleOptions, 82 | ): Promise<BrowserBuildOutput> { 83 | const run = await architect.scheduleTarget(target, overrides, scheduleOptions); 84 | const output = (await run.result) as BuilderOutput & { outputs: { path: string }[] }; 85 | expect(output.success).toBe(true); 86 | 87 | if (!output.success) { 88 | await run.stop(); 89 | 90 | return { 91 | output, 92 | files: {}, 93 | }; 94 | } 95 | 96 | const [{ path }] = output.outputs; 97 | expect(path).toBeTruthy(); 98 | const outputPath = normalize(path); 99 | 100 | const fileNames = await firstValueFrom(host.list(outputPath)); 101 | const files = fileNames.reduce((acc: { [name: string]: Promise<string> }, path) => { 102 | let cache: Promise<string> | null = null; 103 | Object.defineProperty(acc, path, { 104 | enumerable: true, 105 | get() { 106 | if (cache) { 107 | return cache; 108 | } 109 | if (!fileNames.includes(path)) { 110 | return Promise.reject('No file named ' + path); 111 | } 112 | cache = firstValueFrom(host.read(join(outputPath, path))).then((content) => 113 | virtualFs.fileBufferToString(content), 114 | ); 115 | 116 | return cache; 117 | }, 118 | }); 119 | 120 | return acc; 121 | }, {}); 122 | 123 | await run.stop(); 124 | 125 | return { 126 | output, 127 | files, 128 | }; 129 | } 130 | 131 | export const lazyModuleFiles: { [path: string]: string } = { 132 | 'src/app/lazy/lazy-routing.module.ts': ` 133 | import { NgModule } from '@angular/core'; 134 | import { Routes, RouterModule } from '@angular/router'; 135 | 136 | const routes: Routes = []; 137 | 138 | @NgModule({ 139 | imports: [RouterModule.forChild(routes)], 140 | exports: [RouterModule] 141 | }) 142 | export class LazyRoutingModule { } 143 | `, 144 | 'src/app/lazy/lazy.module.ts': ` 145 | import { NgModule } from '@angular/core'; 146 | import { CommonModule } from '@angular/common'; 147 | 148 | import { LazyRoutingModule } from './lazy-routing.module'; 149 | 150 | @NgModule({ 151 | imports: [ 152 | CommonModule, 153 | LazyRoutingModule 154 | ], 155 | declarations: [] 156 | }) 157 | export class LazyModule { } 158 | `, 159 | }; 160 | 161 | export const lazyModuleFnImport: { [path: string]: string } = { 162 | 'src/app/app.module.ts': ` 163 | import { BrowserModule } from '@angular/platform-browser'; 164 | import { NgModule } from '@angular/core'; 165 | 166 | import { AppComponent } from './app.component'; 167 | import { RouterModule } from '@angular/router'; 168 | 169 | @NgModule({ 170 | declarations: [ 171 | AppComponent 172 | ], 173 | imports: [ 174 | BrowserModule, 175 | RouterModule.forRoot([ 176 | { path: 'lazy', loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) } 177 | ]) 178 | ], 179 | providers: [], 180 | bootstrap: [AppComponent] 181 | }) 182 | export class AppModule { } 183 | `, 184 | }; 185 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "cli": { 4 | "analytics": false 5 | }, 6 | "version": 1, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "ngssc-app": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular/build:application", 22 | "options": { 23 | "outputPath": "dist/ngssc-app", 24 | "index": "src/index.html", 25 | "browser": "src/main.ts", 26 | "polyfills": ["zone.js"], 27 | "tsConfig": "tsconfig.app.json", 28 | "assets": ["src/favicon.ico", "src/assets"], 29 | "styles": ["src/styles.css"], 30 | "scripts": [] 31 | }, 32 | "configurations": { 33 | "production": { 34 | "budgets": [ 35 | { 36 | "type": "initial", 37 | "maximumWarning": "500kb", 38 | "maximumError": "1mb" 39 | }, 40 | { 41 | "type": "anyComponentStyle", 42 | "maximumWarning": "2kb", 43 | "maximumError": "4kb" 44 | } 45 | ], 46 | "outputHashing": "all" 47 | }, 48 | "i18n": { 49 | "localize": true, 50 | "budgets": [ 51 | { 52 | "type": "initial", 53 | "maximumWarning": "500kb", 54 | "maximumError": "1mb" 55 | }, 56 | { 57 | "type": "anyComponentStyle", 58 | "maximumWarning": "2kb", 59 | "maximumError": "4kb" 60 | } 61 | ], 62 | "outputHashing": "all" 63 | }, 64 | "development": { 65 | "optimization": false, 66 | "extractLicenses": false, 67 | "sourceMap": true, 68 | "fileReplacements": [ 69 | { 70 | "replace": "src/environments/environment.ts", 71 | "with": "src/environments/environment.development.ts" 72 | } 73 | ] 74 | } 75 | }, 76 | "defaultConfiguration": "production" 77 | }, 78 | "serve": { 79 | "builder": "@angular/build:dev-server", 80 | "configurations": { 81 | "production": { 82 | "buildTarget": "ngssc-app:build:production" 83 | }, 84 | "development": { 85 | "buildTarget": "ngssc-app:build:development" 86 | } 87 | }, 88 | "defaultConfiguration": "development" 89 | }, 90 | "extract-i18n": { 91 | "builder": "@angular/build:extract-i18n", 92 | "options": { 93 | "format": "json", 94 | "outputPath": "src/locales", 95 | "buildTarget": "ngssc-app:build" 96 | } 97 | }, 98 | "test": { 99 | "builder": "@angular/build:karma", 100 | "options": { 101 | "polyfills": ["zone.js", "zone.js/testing"], 102 | "tsConfig": "tsconfig.spec.json", 103 | "assets": ["src/favicon.ico", "src/assets"], 104 | "styles": ["src/styles.css"], 105 | "scripts": [], 106 | "browsers": "ChromeHeadless" 107 | }, 108 | "configurations": { 109 | "ci": { 110 | "watch": false, 111 | "codeCoverage": true 112 | } 113 | } 114 | }, 115 | "lint": { 116 | "builder": "@angular-eslint/builder:lint", 117 | "options": { 118 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 119 | } 120 | }, 121 | "ngsscbuild": { 122 | "builder": "./dist/angular-server-side-configuration:ngsscbuild", 123 | "options": { 124 | "additionalEnvironmentVariables": ["MANUAL_VALUE"], 125 | "buildTarget": "ngssc-app:build" 126 | }, 127 | "configurations": { 128 | "production": { 129 | "buildTarget": "ngssc-app:build:production" 130 | } 131 | } 132 | } 133 | } 134 | }, 135 | "angular-server-side-configuration": { 136 | "projectType": "library", 137 | "root": "projects/angular-server-side-configuration", 138 | "sourceRoot": "projects/angular-server-side-configuration/src", 139 | "prefix": "lib", 140 | "architect": { 141 | "build": { 142 | "builder": "@angular/build:ng-packagr", 143 | "options": { 144 | "project": "projects/angular-server-side-configuration/ng-package.json" 145 | }, 146 | "configurations": { 147 | "production": { 148 | "tsConfig": "projects/angular-server-side-configuration/tsconfig.lib.prod.json" 149 | }, 150 | "development": { 151 | "tsConfig": "projects/angular-server-side-configuration/tsconfig.lib.json" 152 | } 153 | }, 154 | "defaultConfiguration": "production" 155 | }, 156 | "lint": { 157 | "builder": "@angular-eslint/builder:lint", 158 | "options": { 159 | "lintFilePatterns": [ 160 | "projects/angular-server-side-configuration/**/*.ts", 161 | "projects/angular-server-side-configuration/**/*.html" 162 | ] 163 | } 164 | } 165 | } 166 | } 167 | }, 168 | "schematics": { 169 | "@angular-eslint/schematics:application": { 170 | "setParserOptionsProject": true 171 | }, 172 | "@angular-eslint/schematics:library": { 173 | "setParserOptionsProject": true 174 | }, 175 | "@schematics/angular:component": { 176 | "type": "component" 177 | }, 178 | "@schematics/angular:directive": { 179 | "type": "directive" 180 | }, 181 | "@schematics/angular:service": { 182 | "type": "service" 183 | }, 184 | "@schematics/angular:guard": { 185 | "typeSeparator": "." 186 | }, 187 | "@schematics/angular:interceptor": { 188 | "typeSeparator": "." 189 | }, 190 | "@schematics/angular:module": { 191 | "typeSeparator": "." 192 | }, 193 | "@schematics/angular:pipe": { 194 | "typeSeparator": "." 195 | }, 196 | "@schematics/angular:resolver": { 197 | "typeSeparator": "." 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/builders/ngsscbuild/index.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, readFileSync, writeFileSync } from 'fs'; 2 | import { basename, join } from 'path'; 3 | import { 4 | type BuilderContext, 5 | createBuilder, 6 | targetFromTargetString, 7 | } from '@angular-devkit/architect'; 8 | import type { ApplicationBuilderOptions } from '@angular/build'; 9 | import type { json, JsonObject } from '@angular-devkit/core'; 10 | import type { Ngssc } from 'angular-server-side-configuration'; 11 | import * as glob from 'glob'; 12 | import type { Schema } from './schema'; 13 | import { VariableDetector } from './variable-detector'; 14 | import type { NgsscContext } from './ngssc-context'; 15 | 16 | export type NgsscBuildSchema = Schema; 17 | type ApplicationBuilderVariant = undefined | 'browser-only' | 'server'; 18 | 19 | export async function ngsscBuild(options: NgsscBuildSchema, context: BuilderContext) { 20 | const buildTarget = targetFromTargetString(options.buildTarget || options.browserTarget); 21 | const rawBuilderOptions = await context.getTargetOptions(buildTarget); 22 | const builderName = await context.getBuilderNameForTarget(buildTarget); 23 | const builderOptions = await context.validateOptions<json.JsonObject & ApplicationBuilderOptions>( 24 | rawBuilderOptions, 25 | builderName, 26 | ); 27 | const scheduledTarget = await context.scheduleTarget(buildTarget); 28 | const result = await scheduledTarget.result; 29 | if (!result.success) { 30 | const buildConfig = buildTarget.configuration ? `:${buildTarget.configuration}` : ''; 31 | context.logger.warn( 32 | `ngssc: Failed build of ${buildTarget.project}:${buildTarget.target}${buildConfig}. Skipping ngssc build.`, 33 | ); 34 | return result; 35 | } 36 | 37 | await detectVariablesAndBuildNgsscJson( 38 | options, 39 | builderOptions, 40 | context, 41 | false, 42 | builderName !== '@angular-devkit/build-angular:application' 43 | ? undefined 44 | : 'server' in builderOptions && builderOptions.server 45 | ? 'server' 46 | : 'browser-only', 47 | ); 48 | 49 | return result; 50 | } 51 | 52 | export async function detectVariablesAndBuildNgsscJson( 53 | options: NgsscBuildSchema, 54 | builderOptions: ApplicationBuilderOptions, 55 | context: BuilderContext, 56 | multiple: boolean = false, 57 | applicationBuilderVariant: ApplicationBuilderVariant = undefined, 58 | ) { 59 | const ngsscContext = await detectVariables(context, options.searchPattern); 60 | const builderOutputPath = 61 | typeof builderOptions.outputPath === 'string' 62 | ? builderOptions.outputPath 63 | : (builderOptions.outputPath?.base ?? 'dist'); 64 | let outputPath = join(context.workspaceRoot, builderOutputPath); 65 | const ngssc = buildNgssc( 66 | ngsscContext, 67 | options, 68 | builderOptions, 69 | multiple, 70 | applicationBuilderVariant, 71 | ); 72 | 73 | const browserOutputPaths = [join(outputPath, 'browser')]; 74 | if (typeof builderOptions.outputPath !== 'string' && builderOptions.outputPath?.browser) { 75 | browserOutputPaths.unshift(join(outputPath, builderOptions.outputPath.browser)); 76 | } 77 | outputPath = browserOutputPaths.find(existsSync) ?? outputPath; 78 | writeFileSync(join(outputPath, 'ngssc.json'), JSON.stringify(ngssc, null, 2), 'utf8'); 79 | } 80 | 81 | export async function detectVariables( 82 | context: BuilderContext, 83 | searchPattern?: string | null, 84 | ): Promise<NgsscContext> { 85 | const projectName = context.target && context.target.project; 86 | if (!projectName) { 87 | throw new Error('The builder requires a target.'); 88 | } 89 | 90 | const projectMetadata = await context.getProjectMetadata(projectName); 91 | const sourceRoot = projectMetadata['sourceRoot'] as string | undefined; 92 | const defaultSearchPattern = sourceRoot 93 | ? `${sourceRoot}/**/environments/environment*.ts` 94 | : '**/environments/environment*.ts'; 95 | 96 | const detector = new VariableDetector(context.logger); 97 | const typeScriptFiles = await glob.glob(searchPattern || defaultSearchPattern, { 98 | absolute: true, 99 | cwd: context.workspaceRoot, 100 | ignore: ['**/node_modules/**', '**/*.spec.ts', '**/*.d.ts'], 101 | }); 102 | let ngsscContext: NgsscContext | null = null; 103 | for (const file of typeScriptFiles) { 104 | const fileContent = readFileSync(file, 'utf8'); 105 | const innerNgsscContext = detector.detect(fileContent); 106 | if (!innerNgsscContext.variables.length) { 107 | continue; 108 | } else if (!ngsscContext) { 109 | ngsscContext = innerNgsscContext; 110 | continue; 111 | } 112 | if (ngsscContext.variant !== innerNgsscContext.variant) { 113 | context.logger.info( 114 | `ngssc: Detected conflicting variants (${ngsscContext.variant} and ${innerNgsscContext.variant}) being used`, 115 | ); 116 | } 117 | ngsscContext.variables.push( 118 | ...innerNgsscContext.variables.filter((v) => !ngsscContext!.variables.includes(v)), 119 | ); 120 | } 121 | if (!ngsscContext) { 122 | return { variant: 'process', variables: [] }; 123 | } 124 | 125 | context.logger.info( 126 | `ngssc: Detected variant '${ngsscContext.variant}' with variables ` + 127 | `'${ngsscContext.variables.join(', ')}'`, 128 | ); 129 | 130 | return ngsscContext; 131 | } 132 | 133 | export function buildNgssc( 134 | ngsscContext: NgsscContext, 135 | options: NgsscBuildSchema, 136 | builderOptions?: ApplicationBuilderOptions, 137 | multiple: boolean = false, 138 | applicationBuilderVariant: ApplicationBuilderVariant = undefined, 139 | ): Ngssc { 140 | return { 141 | environmentVariables: [ 142 | ...ngsscContext.variables, 143 | ...(options.additionalEnvironmentVariables || []), 144 | ], 145 | filePattern: 146 | options.filePattern || 147 | extractFilePattern(builderOptions, multiple, applicationBuilderVariant), 148 | variant: ngsscContext.variant, 149 | }; 150 | } 151 | 152 | function extractFilePattern( 153 | builderOptions: ApplicationBuilderOptions | undefined, 154 | multiple: boolean, 155 | applicationBuilderVariant: ApplicationBuilderVariant = undefined, 156 | ) { 157 | if (builderOptions && applicationBuilderVariant === 'server') { 158 | return '**/index{.,.server.}html'; 159 | } 160 | 161 | const index = builderOptions?.index; 162 | let result = '**/index.html'; 163 | if (!index || typeof index === 'boolean') { 164 | return result; 165 | } else if (typeof index === 'string') { 166 | result = basename(index); 167 | } else if (index.output) { 168 | result = basename(index.output); 169 | } else { 170 | result = basename(index.input); 171 | } 172 | return multiple && !result.startsWith('*') ? `**/${result}` : result; 173 | } 174 | 175 | export default createBuilder<NgsscBuildSchema & JsonObject>(ngsscBuild); 176 | -------------------------------------------------------------------------------- /modules/testing/builder/src/jasmine-helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.dev/license 7 | */ 8 | 9 | import { BuilderHandlerFn } from '@angular-devkit/architect'; 10 | import { json, logging } from '@angular-devkit/core'; 11 | import { readFileSync } from 'node:fs'; 12 | import { concatMap, count, debounceTime, firstValueFrom, take, timeout } from 'rxjs'; 13 | import { 14 | BuilderHarness, 15 | BuilderHarnessExecutionOptions, 16 | BuilderHarnessExecutionResult, 17 | } from './builder-harness'; 18 | import { host } from './test-utils'; 19 | 20 | /** 21 | * Maximum time for single build/rebuild 22 | * This accounts for CI variability. 23 | */ 24 | export const BUILD_TIMEOUT = 30_000; 25 | 26 | const optionSchemaCache = new Map<string, json.schema.JsonSchema>(); 27 | 28 | export function describeBuilder<T>( 29 | builderHandler: BuilderHandlerFn<T & json.JsonObject>, 30 | options: { name?: string; schemaPath: string }, 31 | specDefinitions: (harness: JasmineBuilderHarness<T>) => void, 32 | ): void { 33 | let optionSchema = optionSchemaCache.get(options.schemaPath); 34 | if (optionSchema === undefined) { 35 | optionSchema = JSON.parse(readFileSync(options.schemaPath, 'utf8')) as json.schema.JsonSchema; 36 | optionSchemaCache.set(options.schemaPath, optionSchema); 37 | } 38 | const harness = new JasmineBuilderHarness<T>(builderHandler, host, { 39 | builderName: options.name, 40 | optionSchema, 41 | }); 42 | 43 | describe(options.name || builderHandler.name, () => { 44 | beforeEach(async () => { 45 | harness.resetProjectMetadata(); 46 | 47 | await host.initialize().toPromise(); 48 | }); 49 | 50 | afterEach(() => host.restore().toPromise()); 51 | 52 | specDefinitions(harness); 53 | }); 54 | } 55 | 56 | export class JasmineBuilderHarness<T> extends BuilderHarness<T> { 57 | expectFile(path: string): HarnessFileMatchers { 58 | return expectFile(path, this); 59 | } 60 | expectDirectory(path: string): HarnessDirectoryMatchers { 61 | return expectDirectory(path, this); 62 | } 63 | 64 | async executeWithCases( 65 | cases: (( 66 | executionResult: BuilderHarnessExecutionResult, 67 | index: number, 68 | ) => void | Promise<void>)[], 69 | options?: Partial<BuilderHarnessExecutionOptions> & { timeout?: number }, 70 | ): Promise<void> { 71 | const executionCount = await firstValueFrom( 72 | this.execute(options).pipe( 73 | timeout(options?.timeout ?? BUILD_TIMEOUT), 74 | debounceTime(100), // This is needed as sometimes 2 events for the same change fire with webpack. 75 | concatMap(async (result, index) => await cases[index](result, index)), 76 | take(cases.length), 77 | count(), 78 | ), 79 | ); 80 | 81 | expect(executionCount).toBe(cases.length); 82 | } 83 | } 84 | 85 | export interface HarnessFileMatchers { 86 | toExist(): boolean; 87 | toNotExist(): boolean; 88 | readonly content: jasmine.ArrayLikeMatchers<string>; 89 | readonly size: jasmine.Matchers<number>; 90 | } 91 | 92 | export interface HarnessDirectoryMatchers { 93 | toExist(): boolean; 94 | toNotExist(): boolean; 95 | } 96 | 97 | /** 98 | * Add a Jasmine expectation filter to an expectation that always fails with a message. 99 | * @param base The base expectation (`expect(...)`) to use. 100 | * @param message The message to provide in the expectation failure. 101 | */ 102 | function createFailureExpectation<T>(base: T, message: string): T { 103 | // Needed typings are not included in the Jasmine types 104 | const expectation = base as T & { 105 | expector: { 106 | addFilter(filter: { 107 | selectComparisonFunc(): () => { pass: boolean; message: string }; 108 | }): typeof expectation.expector; 109 | }; 110 | }; 111 | expectation.expector = expectation.expector.addFilter({ 112 | selectComparisonFunc() { 113 | return () => ({ 114 | pass: false, 115 | message, 116 | }); 117 | }, 118 | }); 119 | 120 | return expectation; 121 | } 122 | 123 | export function expectFile<T>(path: string, harness: BuilderHarness<T>): HarnessFileMatchers { 124 | return { 125 | toExist() { 126 | const exists = harness.hasFile(path); 127 | expect(exists) 128 | .withContext('Expected file to exist: ' + path) 129 | .toBeTrue(); 130 | 131 | return exists; 132 | }, 133 | toNotExist() { 134 | const exists = harness.hasFile(path); 135 | expect(exists) 136 | .withContext('Expected file to exist: ' + path) 137 | .toBeFalse(); 138 | 139 | return !exists; 140 | }, 141 | get content() { 142 | try { 143 | return expect(harness.readFile(path)).withContext(`With file content for '${path}'`); 144 | } catch (e) { 145 | if ((e as NodeJS.ErrnoException).code !== 'ENOENT') { 146 | throw e; 147 | } 148 | 149 | // File does not exist so always fail the expectation 150 | return createFailureExpectation( 151 | expect(''), 152 | `Expected file content but file does not exist: '${path}'`, 153 | ); 154 | } 155 | }, 156 | get size() { 157 | try { 158 | return expect(Buffer.byteLength(harness.readFile(path))).withContext( 159 | `With file size for '${path}'`, 160 | ); 161 | } catch (e) { 162 | if ((e as NodeJS.ErrnoException).code !== 'ENOENT') { 163 | throw e; 164 | } 165 | 166 | // File does not exist so always fail the expectation 167 | return createFailureExpectation( 168 | expect(0), 169 | `Expected file size but file does not exist: '${path}'`, 170 | ); 171 | } 172 | }, 173 | }; 174 | } 175 | 176 | export function expectDirectory<T>( 177 | path: string, 178 | harness: BuilderHarness<T>, 179 | ): HarnessDirectoryMatchers { 180 | return { 181 | toExist() { 182 | const exists = harness.hasDirectory(path); 183 | expect(exists) 184 | .withContext('Expected directory to exist: ' + path) 185 | .toBeTrue(); 186 | 187 | return exists; 188 | }, 189 | toNotExist() { 190 | const exists = harness.hasDirectory(path); 191 | expect(exists) 192 | .withContext('Expected directory to not exist: ' + path) 193 | .toBeFalse(); 194 | 195 | return !exists; 196 | }, 197 | }; 198 | } 199 | 200 | export function expectLog(logs: readonly logging.LogEntry[], message: string | RegExp) { 201 | expect(logs).toContain( 202 | jasmine.objectContaining({ 203 | message: jasmine.stringMatching(message), 204 | }), 205 | ); 206 | } 207 | 208 | export function expectNoLog( 209 | logs: readonly logging.LogEntry[], 210 | message: string | RegExp, 211 | failureMessage?: string, 212 | ) { 213 | expect(logs) 214 | .withContext(failureMessage ?? '') 215 | .not.toContain( 216 | jasmine.objectContaining({ 217 | message: jasmine.stringMatching(message), 218 | }), 219 | ); 220 | } 221 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/src/insert.spec.ts: -------------------------------------------------------------------------------- 1 | import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'fs'; 2 | import { tmpdir } from 'os'; 3 | import { join } from 'path'; 4 | 5 | import { Ngssc, Variant } from './ngssc'; 6 | import { insert } from './insert'; 7 | 8 | describe('insert', () => { 9 | let directory: string = ''; 10 | let subdirectories: string[]; 11 | const ngssc: Ngssc = { 12 | environmentVariables: ['TEST', 'TEST2'], 13 | filePattern: 'index.html', 14 | variant: 'process', 15 | }; 16 | const envTestContent = 'TESTCONTENT'; 17 | const iife = `<script>(function(self){self.process=${JSON.stringify({ 18 | env: { TEST: envTestContent, TEST2: null }, 19 | })};})(window)</script>`; 20 | const globalIife = `<script>(function(self){Object.assign(self,${JSON.stringify({ 21 | TEST: envTestContent, 22 | TEST2: null, 23 | })});})(window)</script>`; 24 | const ngEnvIife = `<script>(function(self){self.NG_ENV=${JSON.stringify({ 25 | TEST: envTestContent, 26 | TEST2: null, 27 | })};})(window)</script>`; 28 | 29 | function createFiles(variant: Variant = 'process') { 30 | const innerNgssc: Ngssc = { ...ngssc, variant }; 31 | writeFileSync(join(directory, 'de/index.html'), indexHtmlContent, 'utf8'); 32 | writeFileSync(join(directory, 'en/index.html'), indexHtmlContentWithoutConfig, 'utf8'); 33 | writeFileSync(join(directory, 'fr/index.html'), indexHtmlContentWithoutTitle, 'utf8'); 34 | writeFileSync(join(directory, 'de/ngssc.json'), JSON.stringify(innerNgssc), 'utf8'); 35 | writeFileSync(join(directory, 'en/ngssc.json'), JSON.stringify(innerNgssc), 'utf8'); 36 | writeFileSync( 37 | join(directory, 'fr/ngssc.json'), 38 | JSON.stringify({ ...innerNgssc, filePattern: undefined }), 39 | 'utf8', 40 | ); 41 | } 42 | 43 | beforeEach(() => { 44 | // tslint:disable-next-line: no-console 45 | console.log = () => void 0; 46 | process.env['TEST'] = envTestContent; 47 | directory = mkdtempSync(join(tmpdir(), 'insert')); 48 | subdirectories = ['de', 'en', 'fr'].map((d) => join(directory, d)); 49 | subdirectories.forEach((d) => mkdirSync(d)); 50 | }); 51 | 52 | it('should throw on missing ngssc.json', () => { 53 | expect(() => insert()).toThrow(); 54 | }); 55 | 56 | it('should do nothing with dry run', () => { 57 | createFiles(); 58 | insert({ directory, recursive: true, dryRun: true }); 59 | for (const file of subdirectories.map((d) => join(d, 'index.html'))) { 60 | expect(readFileSync(file, 'utf8')).not.toContain(iife); 61 | } 62 | }); 63 | 64 | it('should insert into html with root ngssc.json', () => { 65 | createFiles(); 66 | writeFileSync( 67 | join(directory, 'ngssc.json'), 68 | JSON.stringify({ ...ngssc, filePattern: '**/index.html' }), 69 | 'utf8', 70 | ); 71 | insert({ directory }); 72 | for (const file of subdirectories.map((d) => join(d, 'index.html'))) { 73 | expect(readFileSync(file, 'utf8')).toContain(iife); 74 | } 75 | }); 76 | 77 | it('should insert into html with recursive true', () => { 78 | createFiles(); 79 | insert({ directory, recursive: true }); 80 | for (const file of subdirectories.map((d) => join(d, 'index.html'))) { 81 | expect(readFileSync(file, 'utf8')).toContain(iife); 82 | } 83 | }); 84 | 85 | it('should insert idempotent', () => { 86 | createFiles(); 87 | insert({ directory, recursive: true }); 88 | for (const file of subdirectories.map((d) => join(d, 'index.html'))) { 89 | expect(readFileSync(file, 'utf8')).toContain(iife); 90 | } 91 | 92 | const test2Value = 'test2'; 93 | process.env['TEST2'] = test2Value; 94 | const changedIife = 95 | // tslint:disable-next-line: max-line-length 96 | `<script>(function(self){self.process=${JSON.stringify({ 97 | env: { TEST: envTestContent, TEST2: test2Value }, 98 | })};})(window)</script>`; 99 | insert({ directory, recursive: true }); 100 | for (const file of subdirectories.map((d) => join(d, 'index.html'))) { 101 | expect(readFileSync(file, 'utf8')).not.toContain(iife); 102 | expect(readFileSync(file, 'utf8')).toContain(changedIife); 103 | } 104 | delete process.env['TEST2']; 105 | }); 106 | 107 | it('should do nothing on no html files', () => { 108 | writeFileSync(join(directory, 'ngssc.json'), JSON.stringify(ngssc), 'utf8'); 109 | insert({ directory }); 110 | }); 111 | 112 | it('should insert into html with recursive true and variant global', () => { 113 | createFiles('global'); 114 | insert({ directory, recursive: true }); 115 | for (const file of subdirectories.map((d) => join(d, 'index.html'))) { 116 | expect(readFileSync(file, 'utf8')).toContain(globalIife); 117 | } 118 | }); 119 | 120 | it('should insert into html with recursive true and variant NG_ENV', () => { 121 | createFiles('NG_ENV'); 122 | insert({ directory, recursive: true }); 123 | for (const file of subdirectories.map((d) => join(d, 'index.html'))) { 124 | expect(readFileSync(file, 'utf8')).toContain(ngEnvIife); 125 | } 126 | }); 127 | }); 128 | 129 | const indexHtmlContent = `<!doctype html> 130 | <html lang="en"> 131 | <head> 132 | <meta charset="utf-8"> 133 | <title>Test 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | `; 146 | 147 | const indexHtmlContentWithoutConfig = ` 148 | 149 | 150 | 151 | Test 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | `; 164 | 165 | const indexHtmlContentWithoutTitle = ` 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | `; 181 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/schematics/ng-add/index.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join, normalize } from '@angular-devkit/core'; 2 | import { 3 | chain, 4 | type Rule, 5 | type SchematicContext, 6 | SchematicsException, 7 | type Tree, 8 | } from '@angular-devkit/schematics'; 9 | import { InsertChange } from '@schematics/angular/utility/change'; 10 | import { getWorkspace, updateWorkspace } from '@schematics/angular/utility/workspace'; 11 | import type { Schema } from './schema'; 12 | 13 | export function ngAdd(options: Schema): Rule { 14 | return chain([ 15 | addNgsscTargetToWorkspace(options), 16 | addDescriptionToMainFile(options), 17 | addNgsscToPackageScripts(options), 18 | addPlaceholderToIndexHtml(options), 19 | ]); 20 | } 21 | 22 | function addNgsscTargetToWorkspace(options: Schema): Rule { 23 | return (_host: Tree, context: SchematicContext) => 24 | updateWorkspace((workspace) => { 25 | const project = workspace.projects.get(options.project); 26 | if (!project) { 27 | return; 28 | } 29 | 30 | const ngsscOptions = { 31 | additionalEnvironmentVariables: options.additionalEnvironmentVariables 32 | ? options.additionalEnvironmentVariables.split(',').map((e) => e.trim()) 33 | : [], 34 | }; 35 | 36 | const target = project.targets.get('ngsscbuild'); 37 | if (target) { 38 | context.logger.info( 39 | `Skipping adding ngsscbuild target to angular.json, as it already exists in project ${options.project}.`, 40 | ); 41 | return; 42 | } 43 | 44 | project.targets.add({ 45 | name: 'ngsscbuild', 46 | builder: 'angular-server-side-configuration:ngsscbuild', 47 | options: { 48 | ...ngsscOptions, 49 | buildTarget: `${options.project}:build`, 50 | }, 51 | configurations: { 52 | production: { 53 | buildTarget: `${options.project}:build:production`, 54 | }, 55 | }, 56 | }); 57 | }); 58 | } 59 | 60 | function addDescriptionToMainFile(options: Schema): Rule { 61 | const noAppropriateInsertFileWarning = 62 | 'Unable to resolve appropriate file to insert import. Please follow documentation.'; 63 | return async (host: Tree, context: SchematicContext) => { 64 | const { project } = await resolveWorkspace(options, host); 65 | const buildTarget = project.targets.get('build')!; 66 | const mainFile = normalize( 67 | (buildTarget?.options?.['main'] as string) ?? 68 | (buildTarget?.options?.['browser'] as string) ?? 69 | '', 70 | ); 71 | if (!mainFile) { 72 | context.logger.warn(noAppropriateInsertFileWarning); 73 | return; 74 | } 75 | 76 | const insertFile = [ 77 | join(dirname(mainFile), 'environments/environment.prod.ts'), 78 | join(dirname(mainFile), 'environments/environment.ts'), 79 | join(dirname(mainFile), 'app/app.config.ts'), 80 | join(dirname(mainFile), 'app/app.module.ts'), 81 | join(dirname(mainFile), 'app/app.component.ts'), 82 | mainFile, 83 | ].find((f) => host.exists(f)); 84 | if (!insertFile) { 85 | context.logger.warn(noAppropriateInsertFileWarning); 86 | return; 87 | } 88 | const file = host.get(insertFile); 89 | if (!file) { 90 | context.logger.warn(noAppropriateInsertFileWarning); 91 | return; 92 | } else if (file.content.includes('angular-server-side-configuration')) { 93 | context.logger.info( 94 | `Skipping adding import to ${file.path}, since import was already detected.`, 95 | ); 96 | return; 97 | } 98 | 99 | const insertContent = `import 'angular-server-side-configuration/process'; 100 | 101 | /** 102 | * How to use angular-server-side-configuration: 103 | * 104 | * Use process.env['NAME_OF_YOUR_ENVIRONMENT_VARIABLE'] 105 | * 106 | * const stringValue = process.env['STRING_VALUE']; 107 | * const stringValueWithDefault = process.env['STRING_VALUE'] || 'defaultValue'; 108 | * const numberValue = Number(process.env['NUMBER_VALUE']); 109 | * const numberValueWithDefault = Number(process.env['NUMBER_VALUE'] || 10); 110 | * const booleanValue = process.env['BOOLEAN_VALUE'] === 'true'; 111 | * const booleanValueInverted = process.env['BOOLEAN_VALUE_INVERTED'] !== 'false'; 112 | * const complexValue = JSON.parse(process.env['COMPLEX_JSON_VALUE]); 113 | * 114 | * Please note that process.env[variable] cannot be resolved. Please directly use strings. 115 | */ 116 | 117 | `; 118 | 119 | const insertion = new InsertChange(file.path, 0, insertContent); 120 | const recorder = host.beginUpdate(file.path); 121 | recorder.insertLeft(insertion.pos, insertion.toAdd); 122 | host.commitUpdate(recorder); 123 | }; 124 | } 125 | 126 | function addNgsscToPackageScripts(options: Schema): Rule { 127 | return (host: Tree, context: SchematicContext) => { 128 | const pkgPath = '/package.json'; 129 | const buffer = host.read(pkgPath); 130 | if (buffer === null) { 131 | throw new SchematicsException('Could not find package.json'); 132 | } 133 | 134 | const pkg = { scripts: {}, ...JSON.parse(buffer.toString()) }; 135 | if ('build:ngssc' in pkg.scripts) { 136 | context.logger.info(`Skipping adding script to package.json, as it already exists.`); 137 | return; 138 | } 139 | 140 | pkg.scripts['build:ngssc'] = `ng run ${options.project}:ngsscbuild:production`; 141 | host.overwrite(pkgPath, JSON.stringify(pkg, null, 2)); 142 | }; 143 | } 144 | 145 | function addPlaceholderToIndexHtml(options: Schema): Rule { 146 | return async (host: Tree, context: SchematicContext) => { 147 | const { project } = await resolveWorkspace(options, host); 148 | const build = project.targets.get('build'); 149 | if (!build) { 150 | throw new SchematicsException(`Expected a build target in project ${options.project}!`); 151 | } 152 | 153 | const indexPath = 154 | (build.options?.['index'] as string) || 155 | (project.root ? `${project.root}/src/index.html` : 'src/index.html'); 156 | const indexHtml = host.get(indexPath); 157 | if (!indexHtml) { 158 | throw new SchematicsException(`Expected index html ${indexPath} to exist!`); 159 | } 160 | 161 | const indexHtmlContent = indexHtml.content.toString(); 162 | if (//.test(indexHtmlContent)) { 163 | context.logger.info( 164 | `Skipping adding placeholder to ${indexHtml.path}, as it already contains it.`, 165 | ); 166 | return; 167 | } 168 | 169 | const insertIndex = indexHtmlContent.includes('') 170 | ? indexHtmlContent.indexOf('') + 9 171 | : indexHtmlContent.indexOf(''); 172 | const insertion = new InsertChange(indexHtml.path, insertIndex, ' \n'); 173 | const recorder = host.beginUpdate(indexHtml.path); 174 | recorder.insertLeft(insertion.pos, insertion.toAdd); 175 | host.commitUpdate(recorder); 176 | }; 177 | } 178 | 179 | async function resolveWorkspace(options: Schema, host: Tree) { 180 | const workspace = await getWorkspace(host); 181 | const project = workspace.projects.get(options.project); 182 | if (!project) { 183 | throw new SchematicsException(`Project ${options.project} not found!`); 184 | } 185 | 186 | return { workspace, project }; 187 | } 188 | -------------------------------------------------------------------------------- /cli/substitute_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | ) 10 | 11 | func TestSubstitutionWithCwdAndSingleNgsscRecursivelyFound(t *testing.T) { 12 | context := createDefaultContextAndCwd(t) 13 | tmpEnv(t, "TEST_VALUE", "example value") 14 | result := runWithArgs("substitute") 15 | assertSuccess(t, result) 16 | assertEqual(t, context.ReadFile("default.conf"), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 17 | } 18 | 19 | func TestSubstitutionWithCwdAndMultipleNgsscRecursivelyFoundWithSameConfig(t *testing.T) { 20 | context := newTestDir(t) 21 | context.CreateDirectory("de").CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 22 | context.CreateDirectory("en").CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 23 | context.CreateFile("default.conf.template", defaultConfContent) 24 | 25 | chdir(t, context.path) 26 | tmpEnv(t, "TEST_VALUE", "example value") 27 | result := runWithArgs("substitute") 28 | assertSuccess(t, result) 29 | assertEqual(t, context.ReadFile("default.conf"), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 30 | } 31 | 32 | func TestSubstitutionWithCwdAndMissingNgssc(t *testing.T) { 33 | context := newTestDir(t) 34 | context.CreateFile("default.conf.template", defaultConfContent) 35 | 36 | chdir(t, context.path) 37 | result := runWithArgs("substitute") 38 | assertFailure(t, result) 39 | assertContains(t, result.err.Error(), "no ngssc.json files found with", "") 40 | } 41 | 42 | func TestSubstitutionWithCwdAndMultipleNgsscWithDifferentConfigs(t *testing.T) { 43 | context := newTestDir(t) 44 | context.CreateDirectory("de").CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 45 | context.CreateDirectory("en").CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE2"]}`) 46 | context.CreateFile("default.conf.template", defaultConfContent) 47 | 48 | chdir(t, context.path) 49 | result := runWithArgs("substitute") 50 | assertFailure(t, result) 51 | assertContains(t, result.err.Error(), "all recursively found ngssc.json must have same variant and environment variables configuration", "") 52 | } 53 | 54 | func TestSubstitutionWithCwdAndSingleNgsscViaParameter(t *testing.T) { 55 | context := createDefaultContextAndCwd(t) 56 | tmpEnv(t, "TEST_VALUE", "example value") 57 | result := runWithArgs("substitute", "--ngssc-path=ngssc.json") 58 | assertSuccess(t, result) 59 | assertEqual(t, context.ReadFile("default.conf"), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 60 | } 61 | 62 | func TestSubstitutionWithCwdAndSingleNgsscViaDirectoryParameter(t *testing.T) { 63 | context := createDefaultContextAndCwd(t) 64 | tmpEnv(t, "TEST_VALUE", "example value") 65 | result := runWithArgs("substitute", "--ngssc-path=.") 66 | assertSuccess(t, result) 67 | assertEqual(t, context.ReadFile("default.conf"), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 68 | } 69 | 70 | func TestSubstitutionWithCwdAndSingleNgsscViaAbsolutePathParameter(t *testing.T) { 71 | context := createDefaultContextAndCwd(t) 72 | tmpEnv(t, "TEST_VALUE", "example value") 73 | result := runWithArgs("substitute", "--ngssc-path="+filepath.Join(context.path, "ngssc.json")) 74 | assertSuccess(t, result) 75 | assertEqual(t, context.ReadFile("default.conf"), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 76 | } 77 | 78 | func TestSubstitutionWithMissingFile(t *testing.T) { 79 | context := newTestDir(t) 80 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 81 | 82 | chdir(t, context.path) 83 | result := runWithArgs("substitute") 84 | assertFailure(t, result) 85 | assertContains(t, result.err.Error(), "no files found with", "") 86 | } 87 | 88 | func TestSubstitutionWithCwdWithOutAndSingleNgsscRecursivelyFound(t *testing.T) { 89 | context := createDefaultContextAndCwd(t) 90 | tmpEnv(t, "TEST_VALUE", "example value") 91 | result := runWithArgs("substitute", "--out=out") 92 | assertSuccess(t, result) 93 | assertEqual(t, context.ReadFile("out/default.conf"), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 94 | } 95 | 96 | func TestSubstitutionWithCwdWithAbsoluteOutAndSingleNgsscRecursivelyFound(t *testing.T) { 97 | createDefaultContextAndCwd(t) 98 | outDir := t.TempDir() 99 | tmpEnv(t, "TEST_VALUE", "example value") 100 | result := runWithArgs("substitute", fmt.Sprintf(`--out=%v`, outDir)) 101 | assertSuccess(t, result) 102 | outPath := filepath.Join(outDir, "default.conf") 103 | fileContent, err := ioutil.ReadFile(outPath) 104 | if err != nil { 105 | panic(err) 106 | } 107 | assertEqual(t, string(fileContent), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 108 | } 109 | 110 | func TestSubstitutionWithAbsolutePathAndSingleNgsscViaAbsolutePathParameter(t *testing.T) { 111 | context := createDefaultContext(t) 112 | 113 | tmpEnv(t, "TEST_VALUE", "example value") 114 | result := runWithArgs("substitute", "--ngssc-path="+filepath.Join(context.path, "ngssc.json"), context.path) 115 | assertSuccess(t, result) 116 | assertEqual(t, context.ReadFile("default.conf"), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 117 | } 118 | 119 | func TestSubstitutionWithCwdAndSingleNgsscRecursivelyFoundWithSha384(t *testing.T) { 120 | context := createDefaultContextAndCwd(t) 121 | tmpEnv(t, "TEST_VALUE", "example value") 122 | result := runWithArgs("substitute", "--hash-algorithm=sha384") 123 | assertSuccess(t, result) 124 | assertEqual(t, context.ReadFile("default.conf"), sha384ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 125 | } 126 | 127 | func TestSubstitutionWithCwdAndSingleNgsscRecursivelyFoundWithSha384UpperCase(t *testing.T) { 128 | context := createDefaultContextAndCwd(t) 129 | tmpEnv(t, "TEST_VALUE", "example value") 130 | result := runWithArgs("substitute", "--hash-algorithm=SHA384") 131 | assertSuccess(t, result) 132 | assertEqual(t, context.ReadFile("default.conf"), sha384ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 133 | } 134 | 135 | func TestSubstitutionWithCwdAndSingleNgsscRecursivelyFoundWithSha256(t *testing.T) { 136 | context := createDefaultContextAndCwd(t) 137 | tmpEnv(t, "TEST_VALUE", "example value") 138 | result := runWithArgs("substitute", "--hash-algorithm=sha256") 139 | assertSuccess(t, result) 140 | assertEqual(t, context.ReadFile("default.conf"), sha256ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 141 | } 142 | 143 | func TestSubstitutionWithCwdAndSingleNgsscRecursivelyFoundWithSha256UpperCase(t *testing.T) { 144 | context := createDefaultContextAndCwd(t) 145 | tmpEnv(t, "TEST_VALUE", "example value") 146 | result := runWithArgs("substitute", "--hash-algorithm=SHA256") 147 | assertSuccess(t, result) 148 | assertEqual(t, context.ReadFile("default.conf"), sha256ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 149 | } 150 | 151 | func TestSubstitutionWithCwdAndSingleNgsscRecursivelyFoundWithInvalidHash(t *testing.T) { 152 | context := createDefaultContextAndCwd(t) 153 | tmpEnv(t, "TEST_VALUE", "example value") 154 | result := runWithArgs("substitute", "--hash-algorithm=invalid") 155 | assertSuccess(t, result) 156 | assertEqual(t, context.ReadFile("default.conf"), sha512ConfContent, "Expected ${NGSSC_CSP_HASH} to be substituted.") 157 | assertContains(t, result.Stdout(), "Unknown hash algorithm invalid. Using sha512 instead.", "") 158 | } 159 | 160 | func TestSubstitutionWithIncludingEnvironmentVariables(t *testing.T) { 161 | context := newTestDir(t) 162 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 163 | context.CreateFile( 164 | "default.conf.template", 165 | "test $NGSSC_CSP_HASH some text ${NGSSC_CSP_HASH} other ${TEST_VALUE} some text $TEST_VALUE ${MISSING_VALUE}") 166 | 167 | chdir(t, context.path) 168 | tmpEnv(t, "TEST_VALUE", "example value") 169 | result := runWithArgs("substitute", "--include-env") 170 | assertSuccess(t, result) 171 | assertEqual( 172 | t, 173 | context.ReadFile("default.conf"), 174 | "test $NGSSC_CSP_HASH some text 'sha512-21jOIuJ7NOssNu09erK1ht/C+K5ebRhhCGtsIfs5W5F4GkJ5mHbXk4lRA6i/cAM/3FNcyHnR0heOe6ZVrOzmgQ=='"+ 175 | " other example value some text $TEST_VALUE ${MISSING_VALUE}", 176 | "Expected environment variables to be substituted.") 177 | } 178 | 179 | func TestSubstitutionDryRun(t *testing.T) { 180 | context := createDefaultContextAndCwd(t) 181 | tmpEnv(t, "TEST_VALUE", "example value") 182 | result := runWithArgs("substitute", "--dry") 183 | assertSuccess(t, result) 184 | _, err := os.Stat(filepath.Join(context.path, "default.conf")) 185 | assertTrue(t, os.IsNotExist(err), "Expected default.conf to not have been created") 186 | } 187 | 188 | func TestSubstitutionMissingTemplateDirectory(t *testing.T) { 189 | context := newTestDir(t) 190 | result := runWithArgs("substitute", filepath.Join(context.path, "missing")) 191 | assertFailure(t, result) 192 | assertContains(t, result.err.Error(), "template directory does not exist:", "") 193 | } 194 | 195 | func TestSubstitutionMissingNgssc(t *testing.T) { 196 | context := newTestDir(t) 197 | result := runWithArgs("substitute", "--ngssc-path="+filepath.Join(context.path, "missing")) 198 | assertFailure(t, result) 199 | assertContains(t, result.err.Error(), "no ngssc.json files found", "") 200 | } 201 | 202 | func TestSubstitutionWithNoVariables(t *testing.T) { 203 | context := createDefaultContextAndCwd(t) 204 | context.CreateFile("default.conf.template", "no variables") 205 | tmpEnv(t, "TEST_VALUE", "example value") 206 | result := runWithArgs("substitute") 207 | assertSuccess(t, result) 208 | assertEqual(t, context.ReadFile("default.conf"), "no variables", "") 209 | } 210 | 211 | func createDefaultContextAndCwd(t *testing.T) TestDir { 212 | context := createDefaultContext(t) 213 | chdir(t, context.path) 214 | return context 215 | } 216 | 217 | func createDefaultContext(t *testing.T) TestDir { 218 | context := newTestDir(t) 219 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 220 | context.CreateFile("default.conf.template", defaultConfContent) 221 | return context 222 | } 223 | 224 | var defaultConfContent = "test $NGSSC_CSP_HASH some text ${NGSSC_CSP_HASH} other" 225 | var sha512ConfContent = `test $NGSSC_CSP_HASH some text 'sha512-21jOIuJ7NOssNu09erK1ht/C+K5ebRhhCGtsIfs5W5F4GkJ5mHbXk4lRA6i/cAM/3FNcyHnR0heOe6ZVrOzmgQ==' other` 226 | var sha384ConfContent = `test $NGSSC_CSP_HASH some text 'sha384-YvhmrZwFM9YFATGGVVvaJ9nrHGj8IOvUTGa7hlPAYvVfz7C6yqd3qtFL/KGohBs1' other` 227 | var sha256ConfContent = `test $NGSSC_CSP_HASH some text 'sha256-Mc7pndp1wggP7pIEfFqPE1e7KD6BdhIMvqXVAonw32s=' other` 228 | -------------------------------------------------------------------------------- /projects/angular-server-side-configuration/src/glob-to-regexp.spec.ts: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/fitzgen/glob-to-regexp/blob/master/test.js 2 | 3 | import { globToRegExp, Options } from './glob-to-regexp'; 4 | 5 | describe('globToRegExp', () => { 6 | function assertMatch(glob: string, str: string, opts?: Options) { 7 | expect(globToRegExp(glob, opts).test(str)).toBeTruthy(); 8 | } 9 | 10 | function assertNotMatch(glob: string, str: string, opts?: Options) { 11 | expect(globToRegExp(glob, opts).test(str)).toBeFalsy(); 12 | } 13 | 14 | function test(globstar: boolean) { 15 | // Match everything 16 | assertMatch('*', 'foo'); 17 | assertMatch('*', 'foo', { flags: 'g' }); 18 | 19 | // Match the end 20 | assertMatch('f*', 'foo'); 21 | assertMatch('f*', 'foo', { flags: 'g' }); 22 | 23 | // Match the start 24 | assertMatch('*o', 'foo'); 25 | assertMatch('*o', 'foo', { flags: 'g' }); 26 | 27 | // Match the middle 28 | assertMatch('f*uck', 'firetruck'); 29 | assertMatch('f*uck', 'firetruck', { flags: 'g' }); 30 | 31 | // Don't match without Regexp 'g' 32 | assertNotMatch('uc', 'firetruck'); 33 | // Match anywhere with RegExp 'g' 34 | assertMatch('uc', 'firetruck', { flags: 'g' }); 35 | 36 | // Match zero characters 37 | assertMatch('f*uck', 'fuck'); 38 | assertMatch('f*uck', 'fuck', { flags: 'g' }); 39 | 40 | // More complex matches 41 | assertMatch('*.min.js', 'http://example.com/jquery.min.js', { globstar: false }); 42 | assertMatch('*.min.*', 'http://example.com/jquery.min.js', { globstar: false }); 43 | assertMatch('*/js/*.js', 'http://example.com/js/jquery.min.js', { globstar: false }); 44 | 45 | // More complex matches with RegExp 'g' flag (complex regression) 46 | assertMatch('*.min.*', 'http://example.com/jquery.min.js', { flags: 'g' }); 47 | assertMatch('*.min.js', 'http://example.com/jquery.min.js', { flags: 'g' }); 48 | assertMatch('*/js/*.js', 'http://example.com/js/jquery.min.js', { flags: 'g' }); 49 | 50 | // Test string "\\\\/$^+?.()=!|{},[].*" represents \\/$^+?.()=!|{},[].* 51 | // The equivalent regex is: /^\\\/\$\^\+\?\.\(\)\=\!\|\{\}\,\[\]\..*$/ 52 | // Both glob and regex match: \/$^+?.()=!|{},[].* 53 | var testStr = '\\\\/$^+?.()=!|{},[].*'; 54 | var targetStr = '\\/$^+?.()=!|{},[].*'; 55 | assertMatch(testStr, targetStr); 56 | assertMatch(testStr, targetStr, { flags: 'g' }); 57 | 58 | // Equivalent matches without/with using RegExp 'g' 59 | assertNotMatch('.min.', 'http://example.com/jquery.min.js'); 60 | assertMatch('*.min.*', 'http://example.com/jquery.min.js'); 61 | assertMatch('.min.', 'http://example.com/jquery.min.js', { flags: 'g' }); 62 | 63 | assertNotMatch('http:', 'http://example.com/jquery.min.js'); 64 | assertMatch('http:*', 'http://example.com/jquery.min.js'); 65 | assertMatch('http:', 'http://example.com/jquery.min.js', { flags: 'g' }); 66 | 67 | assertNotMatch('min.js', 'http://example.com/jquery.min.js'); 68 | assertMatch('*.min.js', 'http://example.com/jquery.min.js'); 69 | assertMatch('min.js', 'http://example.com/jquery.min.js', { flags: 'g' }); 70 | 71 | // Match anywhere (globally) using RegExp 'g' 72 | assertMatch('min', 'http://example.com/jquery.min.js', { flags: 'g' }); 73 | assertMatch('/js/', 'http://example.com/js/jquery.min.js', { flags: 'g' }); 74 | 75 | assertNotMatch('/js*jq*.js', 'http://example.com/js/jquery.min.js'); 76 | assertMatch('/js*jq*.js', 'http://example.com/js/jquery.min.js', { flags: 'g' }); 77 | 78 | // Extended mode 79 | 80 | // ?: Match one character, no more and no less 81 | assertMatch('f?o', 'foo', { extended: true }); 82 | assertNotMatch('f?o', 'fooo', { extended: true }); 83 | assertNotMatch('f?oo', 'foo', { extended: true }); 84 | 85 | // ?: Match one character with RegExp 'g' 86 | assertMatch('f?o', 'foo', { extended: true, globstar: globstar, flags: 'g' }); 87 | assertMatch('f?o', 'fooo', { extended: true, globstar: globstar, flags: 'g' }); 88 | assertMatch('f?o?', 'fooo', { extended: true, globstar: globstar, flags: 'g' }); 89 | assertNotMatch('?fo', 'fooo', { extended: true, globstar: globstar, flags: 'g' }); 90 | assertNotMatch('f?oo', 'foo', { extended: true, globstar: globstar, flags: 'g' }); 91 | assertNotMatch('foo?', 'foo', { extended: true, globstar: globstar, flags: 'g' }); 92 | 93 | // []: Match a character range 94 | assertMatch('fo[oz]', 'foo', { extended: true }); 95 | assertMatch('fo[oz]', 'foz', { extended: true }); 96 | assertNotMatch('fo[oz]', 'fog', { extended: true }); 97 | 98 | // []: Match a character range and RegExp 'g' (regresion) 99 | assertMatch('fo[oz]', 'foo', { extended: true, globstar: globstar, flags: 'g' }); 100 | assertMatch('fo[oz]', 'foz', { extended: true, globstar: globstar, flags: 'g' }); 101 | assertNotMatch('fo[oz]', 'fog', { extended: true, globstar: globstar, flags: 'g' }); 102 | 103 | // {}: Match a choice of different substrings 104 | assertMatch('foo{bar,baaz}', 'foobaaz', { extended: true }); 105 | assertMatch('foo{bar,baaz}', 'foobar', { extended: true }); 106 | assertNotMatch('foo{bar,baaz}', 'foobuzz', { extended: true }); 107 | assertMatch('foo{bar,b*z}', 'foobuzz', { extended: true }); 108 | 109 | // {}: Match a choice of different substrings and RegExp 'g' (regression) 110 | assertMatch('foo{bar,baaz}', 'foobaaz', { extended: true, globstar: globstar, flags: 'g' }); 111 | assertMatch('foo{bar,baaz}', 'foobar', { extended: true, globstar: globstar, flags: 'g' }); 112 | assertNotMatch('foo{bar,baaz}', 'foobuzz', { extended: true, globstar: globstar, flags: 'g' }); 113 | assertMatch('foo{bar,b*z}', 'foobuzz', { extended: true, globstar: globstar, flags: 'g' }); 114 | 115 | // More complex extended matches 116 | assertMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://foo.baaz.com/jquery.min.js', { 117 | extended: true, 118 | }); 119 | assertMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://moz.buzz.com/index.html', { 120 | extended: true, 121 | }); 122 | assertNotMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://moz.buzz.com/index.htm', { 123 | extended: true, 124 | }); 125 | assertNotMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://moz.bar.com/index.html', { 126 | extended: true, 127 | }); 128 | assertNotMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://flozz.buzz.com/index.html', { 129 | extended: true, 130 | }); 131 | 132 | // More complex extended matches and RegExp 'g' (regresion) 133 | assertMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://foo.baaz.com/jquery.min.js', { 134 | extended: true, 135 | globstar: globstar, 136 | flags: 'g', 137 | }); 138 | assertMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://moz.buzz.com/index.html', { 139 | extended: true, 140 | globstar: globstar, 141 | flags: 'g', 142 | }); 143 | assertNotMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://moz.buzz.com/index.htm', { 144 | extended: true, 145 | globstar: globstar, 146 | flags: 'g', 147 | }); 148 | assertNotMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://moz.bar.com/index.html', { 149 | extended: true, 150 | globstar: globstar, 151 | flags: 'g', 152 | }); 153 | assertNotMatch('http://?o[oz].b*z.com/{*.js,*.html}', 'http://flozz.buzz.com/index.html', { 154 | extended: true, 155 | globstar: globstar, 156 | flags: 'g', 157 | }); 158 | 159 | // globstar 160 | assertMatch('http://foo.com/**/{*.js,*.html}', 'http://foo.com/bar/jquery.min.js', { 161 | extended: true, 162 | globstar: globstar, 163 | flags: 'g', 164 | }); 165 | assertMatch('http://foo.com/**/{*.js,*.html}', 'http://foo.com/bar/baz/jquery.min.js', { 166 | extended: true, 167 | globstar: globstar, 168 | flags: 'g', 169 | }); 170 | assertMatch('http://foo.com/**', 'http://foo.com/bar/baz/jquery.min.js', { 171 | extended: true, 172 | globstar: globstar, 173 | flags: 'g', 174 | }); 175 | 176 | // Remaining special chars should still match themselves 177 | // Test string "\\\\/$^+.()=!|,.*" represents \\/$^+.()=!|,.* 178 | // The equivalent regex is: /^\\\/\$\^\+\.\(\)\=\!\|\,\..*$/ 179 | // Both glob and regex match: \/$^+.()=!|,.* 180 | var testExtStr = '\\\\/$^+.()=!|,.*'; 181 | var targetExtStr = '\\/$^+.()=!|,.*'; 182 | assertMatch(testExtStr, targetExtStr, { extended: true }); 183 | assertMatch(testExtStr, targetExtStr, { extended: true, globstar: globstar, flags: 'g' }); 184 | } 185 | 186 | it('should work for all cases', () => { 187 | // regression 188 | // globstar false 189 | test(false); 190 | // globstar true 191 | test(true); 192 | 193 | // globstar specific tests 194 | assertMatch('/foo/*', '/foo/bar.txt', { globstar: true }); 195 | assertMatch('/foo/**', '/foo/baz.txt', { globstar: true }); 196 | assertMatch('/foo/**', '/foo/bar/baz.txt', { globstar: true }); 197 | assertMatch('/foo/*/*.txt', '/foo/bar/baz.txt', { globstar: true }); 198 | assertMatch('/foo/**/*.txt', '/foo/bar/baz.txt', { globstar: true }); 199 | assertMatch('/foo/**/*.txt', '/foo/bar/baz/qux.txt', { globstar: true }); 200 | assertMatch('/foo/**/bar.txt', '/foo/bar.txt', { globstar: true }); 201 | assertMatch('/foo/**/**/bar.txt', '/foo/bar.txt', { globstar: true }); 202 | assertMatch('/foo/**/*/baz.txt', '/foo/bar/baz.txt', { globstar: true }); 203 | assertMatch('/foo/**/*.txt', '/foo/bar.txt', { globstar: true }); 204 | assertMatch('/foo/**/**/*.txt', '/foo/bar.txt', { globstar: true }); 205 | assertMatch('/foo/**/*/*.txt', '/foo/bar/baz.txt', { globstar: true }); 206 | assertMatch('**/*.txt', '/foo/bar/baz/qux.txt', { globstar: true }); 207 | assertMatch('**/foo.txt', 'foo.txt', { globstar: true }); 208 | assertMatch('**/*.txt', 'foo.txt', { globstar: true }); 209 | 210 | assertNotMatch('/foo/*', '/foo/bar/baz.txt', { globstar: true }); 211 | assertNotMatch('/foo/*.txt', '/foo/bar/baz.txt', { globstar: true }); 212 | assertNotMatch('/foo/*/*.txt', '/foo/bar/baz/qux.txt', { globstar: true }); 213 | assertNotMatch('/foo/*/bar.txt', '/foo/bar.txt', { globstar: true }); 214 | assertNotMatch('/foo/*/*/baz.txt', '/foo/bar/baz.txt', { globstar: true }); 215 | assertNotMatch('/foo/**.txt', '/foo/bar/baz/qux.txt', { globstar: true }); 216 | assertNotMatch('/foo/bar**/*.txt', '/foo/bar/baz/qux.txt', { globstar: true }); 217 | assertNotMatch('/foo/bar**', '/foo/bar/baz.txt', { globstar: true }); 218 | assertNotMatch('**/.txt', '/foo/bar/baz/qux.txt', { globstar: true }); 219 | assertNotMatch('*/*.txt', '/foo/bar/baz/qux.txt', { globstar: true }); 220 | assertNotMatch('*/*.txt', 'foo.txt', { globstar: true }); 221 | 222 | assertNotMatch('http://foo.com/*', 'http://foo.com/bar/baz/jquery.min.js', { 223 | extended: true, 224 | globstar: true, 225 | }); 226 | assertNotMatch('http://foo.com/*', 'http://foo.com/bar/baz/jquery.min.js', { globstar: true }); 227 | 228 | assertMatch('http://foo.com/*', 'http://foo.com/bar/baz/jquery.min.js', { globstar: false }); 229 | assertMatch('http://foo.com/**', 'http://foo.com/bar/baz/jquery.min.js', { globstar: true }); 230 | 231 | assertMatch('http://foo.com/*/*/jquery.min.js', 'http://foo.com/bar/baz/jquery.min.js', { 232 | globstar: true, 233 | }); 234 | assertMatch('http://foo.com/**/jquery.min.js', 'http://foo.com/bar/baz/jquery.min.js', { 235 | globstar: true, 236 | }); 237 | assertMatch('http://foo.com/*/*/jquery.min.js', 'http://foo.com/bar/baz/jquery.min.js', { 238 | globstar: false, 239 | }); 240 | assertMatch('http://foo.com/*/jquery.min.js', 'http://foo.com/bar/baz/jquery.min.js', { 241 | globstar: false, 242 | }); 243 | assertNotMatch('http://foo.com/*/jquery.min.js', 'http://foo.com/bar/baz/jquery.min.js', { 244 | globstar: true, 245 | }); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Lukas Spirig 190 | Copyright 2019 Daniel Habenicht (cli) 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /cli/insert_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestNgsscProcessWithNotSetEnvironmentVariable(t *testing.T) { 11 | context := newTestDir(t) 12 | context.CreateFile("index.html", configHTMLTemplate) 13 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 14 | 15 | result := runWithArgs("insert", context.path) 16 | assertSuccess(t, result) 17 | expect := `` 18 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 19 | } 20 | 21 | func TestNgsscProcessWithSetEnvironmentVariable(t *testing.T) { 22 | context := newTestDir(t) 23 | context.CreateFile("index.html", configHTMLTemplate) 24 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 25 | 26 | os.Setenv("TEST_VALUE", "example value") 27 | result := runWithArgs("insert", context.path) 28 | os.Unsetenv("TEST_VALUE") 29 | assertSuccess(t, result) 30 | expect := `` 31 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 32 | } 33 | 34 | func TestNgsscProcessWithSetEnvironmentVariableAndCwd(t *testing.T) { 35 | context := newTestDir(t) 36 | context.CreateFile("index.html", configHTMLTemplate) 37 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 38 | 39 | chdir(t, context.path) 40 | os.Setenv("TEST_VALUE", "example value") 41 | result := runWithArgs("insert") 42 | os.Unsetenv("TEST_VALUE") 43 | assertSuccess(t, result) 44 | expect := `` 45 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 46 | } 47 | 48 | func TestIdempotentNgsscProcessWithSetEnvironmentVariable(t *testing.T) { 49 | context := newTestDir(t) 50 | context.CreateFile("index.html", configHTMLTemplate) 51 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE","TEST_VALUE2"]}`) 52 | 53 | os.Setenv("TEST_VALUE", "example value") 54 | result := runWithArgs("insert", context.path) 55 | os.Unsetenv("TEST_VALUE") 56 | assertSuccess(t, result) 57 | expect := `` 58 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 59 | 60 | os.Setenv("TEST_VALUE", "example value") 61 | os.Setenv("TEST_VALUE2", "example value 2") 62 | result = runWithArgs("insert", context.path) 63 | os.Unsetenv("TEST_VALUE") 64 | os.Unsetenv("TEST_VALUE2") 65 | assertSuccess(t, result) 66 | expect = `` 67 | assertNotContains(t, context.ReadFile("index.html"), expect, "Expected html to contain updated iife.") 68 | expect = `` 69 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain updated iife.") 70 | } 71 | 72 | func TestNgsscGlobalWithSetEnvironmentVariable(t *testing.T) { 73 | context := newTestDir(t) 74 | context.CreateFile("index.html", configHTMLTemplate) 75 | context.CreateFile("ngssc.json", `{"variant":"global","environmentVariables":["TEST_VALUE"]}`) 76 | 77 | os.Setenv("TEST_VALUE", "example value") 78 | result := runWithArgs("insert", context.path) 79 | os.Unsetenv("TEST_VALUE") 80 | assertSuccess(t, result) 81 | expect := `` 82 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 83 | } 84 | 85 | func TestNgsscNgEnvWithNotSetEnvironmentVariable(t *testing.T) { 86 | context := newTestDir(t) 87 | context.CreateFile("index.html", configHTMLTemplate) 88 | context.CreateFile("ngssc.json", `{"variant":"NG_ENV","environmentVariables":["TEST_VALUE"]}`) 89 | 90 | result := runWithArgs("insert", context.path) 91 | assertSuccess(t, result) 92 | expect := `` 93 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 94 | } 95 | 96 | func TestNgsscWithInvalidDir(t *testing.T) { 97 | context := newTestDir(t) 98 | 99 | result := runWithArgs("insert", filepath.Join(context.path, "invalid")) 100 | assertFailure(t, result) 101 | assertContains(t, result.err.Error(), "working directory does not exist", "") 102 | } 103 | 104 | func TestNgsscNgEnvWithSetEnvironmentVariable(t *testing.T) { 105 | context := newTestDir(t) 106 | context.CreateFile("index.html", configHTMLTemplate) 107 | context.CreateFile("ngssc.json", `{"variant":"NG_ENV","environmentVariables":["TEST_VALUE"]}`) 108 | 109 | os.Setenv("TEST_VALUE", "example value") 110 | result := runWithArgs("insert", context.path) 111 | os.Unsetenv("TEST_VALUE") 112 | assertSuccess(t, result) 113 | expect := `` 114 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 115 | } 116 | 117 | func TestNgsscNgEnvWithMultipleHtmlFiles(t *testing.T) { 118 | context := newTestDir(t) 119 | context.CreateFile("ngssc.json", `{"variant":"NG_ENV","environmentVariables":["TEST_VALUE"]}`) 120 | deContext := context.CreateDirectory("de") 121 | deContext.CreateFile("index.html", configHTMLTemplate) 122 | enContext := context.CreateDirectory("en") 123 | enContext.CreateFile("index.html", configHTMLTemplate) 124 | 125 | result := runWithArgs("insert", context.path) 126 | assertSuccess(t, result) 127 | expect := `` 128 | assertContains(t, deContext.ReadFile("index.html"), expect, "Expected html to contain iife.") 129 | expect = `` 130 | assertContains(t, enContext.ReadFile("index.html"), expect, "Expected html to contain iife.") 131 | } 132 | 133 | func TestNgsscNgEnvWithFilePattern(t *testing.T) { 134 | context := newTestDir(t) 135 | context.CreateFile("main.html", configHTMLTemplate) 136 | context.CreateFile("ngssc.json", `{"variant":"NG_ENV","environmentVariables":["TEST_VALUE"],"filePattern":"main.html"}`) 137 | 138 | os.Setenv("TEST_VALUE", "example value") 139 | result := runWithArgs("insert", context.path) 140 | os.Unsetenv("TEST_VALUE") 141 | assertSuccess(t, result) 142 | expect := `` 143 | assertContains(t, context.ReadFile("main.html"), expect, "Expected html to contain iife.") 144 | } 145 | 146 | func TestNgsscProcessWithInsertInHead(t *testing.T) { 147 | context := newTestDir(t) 148 | context.CreateFile("index.html", htmlTemplate) 149 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 150 | 151 | result := runWithArgs("insert", context.path) 152 | assertSuccess(t, result) 153 | expect := `` 154 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 155 | } 156 | 157 | func TestNgsscRecursive(t *testing.T) { 158 | context := newTestDir(t) 159 | deContext := context.CreateDirectory("de") 160 | deContext.CreateFile("index.html", configHTMLTemplate) 161 | deContext.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"],"filePattern":"index.html"}`) 162 | enContext := context.CreateDirectory("en") 163 | enContext.CreateFile("index.html", configHTMLTemplate) 164 | enContext.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"],"filePattern":"index.html"}`) 165 | 166 | result := runWithArgs("insert", context.path, "--recursive") 167 | assertSuccess(t, result) 168 | expect := `` 169 | assertContains(t, deContext.ReadFile("index.html"), expect, "Expected html to contain iife.") 170 | expect = `` 171 | assertContains(t, enContext.ReadFile("index.html"), expect, "Expected html to contain iife.") 172 | } 173 | 174 | func TestNgsscProcessWithNgsw(t *testing.T) { 175 | context := newTestDir(t) 176 | context.CreateFile("index.html", configHTMLTemplate) 177 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 178 | context.CreateFile("ngsw.json", ngswTemplate) 179 | 180 | os.Setenv("TEST_VALUE", "example value") 181 | result := runWithArgs("insert", context.path) 182 | os.Unsetenv("TEST_VALUE") 183 | assertSuccess(t, result) 184 | expect := `` 185 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 186 | assertTrue( 187 | t, 188 | result.StdoutContains("Detected ngsw.json and updated index hash at "), 189 | "Expected ngsw.json to be updated:\n "+result.Stdout()) 190 | } 191 | 192 | func TestNonce(t *testing.T) { 193 | context := newTestDir(t) 194 | context.CreateFile("index.html", configHTMLTemplate) 195 | context.CreateFile("ngssc.json", `{"variant":"process","environmentVariables":["TEST_VALUE"]}`) 196 | 197 | result := runWithArgs("insert", context.path, "--nonce=CSP_NONCE") 198 | assertSuccess(t, result) 199 | expect := `` 200 | assertContains(t, context.ReadFile("index.html"), expect, "Expected html to contain iife.") 201 | } 202 | 203 | var configHTMLTemplate = ` 204 | 205 | 206 | 207 | 208 | Docker - Angular Runtime Variables Demo 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | ` 224 | 225 | var htmlTemplate = strings.Replace(configHTMLTemplate, "", "", 1) 226 | 227 | var ngswTemplate = `{ 228 | "configVersion": 1, 229 | "timestamp": 1602890563070, 230 | "index": "/index.html", 231 | "assetGroups": [ 232 | { 233 | "name": "app", 234 | "installMode": "prefetch", 235 | "updateMode": "prefetch", 236 | "cacheQueryOptions": { 237 | "ignoreVary": true 238 | }, 239 | "urls": [ 240 | "/index.html", 241 | "/main-es2015.456948156f706a9ecc0d.js", 242 | "/main-es5.456948156f706a9ecc0d.js", 243 | "/manifest.webmanifest", 244 | "/polyfills-es2015.a0fa45e0fa52702b64f0.js", 245 | "/polyfills-es5.2dcde1efe3c1bf4aaa25.js", 246 | "/runtime-es2015.409e6590615fb48d139f.js", 247 | "/runtime-es5.409e6590615fb48d139f.js" 248 | ], 249 | "patterns": [] 250 | }, 251 | { 252 | "name": "assets", 253 | "installMode": "lazy", 254 | "updateMode": "prefetch", 255 | "cacheQueryOptions": { 256 | "ignoreVary": true 257 | }, 258 | "urls": [], 259 | "patterns": [] 260 | } 261 | ], 262 | "dataGroups": [], 263 | "hashTable": { 264 | "/index.html": "ea58d338769b6747a0c604d72ac82cf1129ffbb1", 265 | "/main-es2015.456948156f706a9ecc0d.js": "49c43df3989b0bb3c649d699ad5c9e88321048c5", 266 | "/main-es5.456948156f706a9ecc0d.js": "4afe362dbbc6752967d7861a2d57174780659c14", 267 | "/manifest.webmanifest": "2a943f564a370150f1b0cfabe19e5ef8b341dd75", 268 | "/polyfills-es2015.a0fa45e0fa52702b64f0.js": "72ad4ccc0a3916ae4598199447cdeadd6d380570", 269 | "/polyfills-es5.2dcde1efe3c1bf4aaa25.js": "8aa26ea87b9958c6bd13b7c257c0e9940438e684", 270 | "/runtime-es2015.409e6590615fb48d139f.js": "a9aafcf49f49145093fc831efd9b8e2f6c71bb9c", 271 | "/runtime-es5.409e6590615fb48d139f.js": "a9aafcf49f49145093fc831efd9b8e2f6c71bb9c" 272 | }, 273 | "navigationUrls": [ 274 | { 275 | "positive": true, 276 | "regex": "^\\/.*$" 277 | }, 278 | { 279 | "positive": false, 280 | "regex": "^\\/(?:.+\\/)?[^/]*\\.[^/]*$" 281 | }, 282 | { 283 | "positive": false, 284 | "regex": "^\\/(?:.+\\/)?[^/]*__[^/]*$" 285 | }, 286 | { 287 | "positive": false, 288 | "regex": "^\\/(?:.+\\/)?[^/]*__[^/]*\\/.*$" 289 | } 290 | ] 291 | }` 292 | --------------------------------------------------------------------------------