├── .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 | "",
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 };
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 {
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 }, path) => {
102 | let cache: Promise | 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(
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 {
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(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();
27 |
28 | export function describeBuilder(
29 | builderHandler: BuilderHandlerFn,
30 | options: { name?: string; schemaPath: string },
31 | specDefinitions: (harness: JasmineBuilderHarness) => 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(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 extends BuilderHarness {
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)[],
69 | options?: Partial & { timeout?: number },
70 | ): Promise {
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;
89 | readonly size: jasmine.Matchers;
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(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(path: string, harness: BuilderHarness): 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(
177 | path: string,
178 | harness: BuilderHarness,
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 = ``;
20 | const globalIife = ``;
24 | const ngEnvIife = ``;
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 | ``;
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 = `
130 |
131 |
132 |
133 | 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 |
--------------------------------------------------------------------------------