From 357cdf22848dce4c836ebb4ee635bb0aaf86b3dc Mon Sep 17 00:00:00 2001 From: Olivier Combe Date: Thu, 1 Feb 2024 21:56:52 +0100 Subject: [PATCH] feat: support define and context env variables for apps --- .../angular-app-type/angular.application.ts | 22 ++++++++- .../angular-app-type/application.bundler.ts | 23 ++++----- .../application.dev-server.ts | 19 ++++---- .../app-types/angular-app-type/component.json | 1 + .../angular-app-type/plugins/define.plugin.ts | 48 +++++++++++++++++++ .../build-angular/builders/application.ts | 6 ++- .../schemas/application.schema.ts | 6 +++ 7 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 angular/app-types/angular-app-type/plugins/define.plugin.ts diff --git a/angular/app-types/angular-app-type/angular.application.ts b/angular/app-types/angular-app-type/angular.application.ts index f63f9d8..10d7a7a 100644 --- a/angular/app-types/angular-app-type/angular.application.ts +++ b/angular/app-types/angular-app-type/angular.application.ts @@ -146,6 +146,16 @@ export class AngularApp implements Application { return context as any as EnvContext; } + private async getEnvFile(mode: string, rootDir: string, overrides?: Record) { + // TODO: enable this one we have ESM envs, otherwise we get a warning message about loading the deprecated CJS build of Vite + // const vite = await loadEsmModule('vite'); + // const dotenv = vite.loadEnv(mode, rootDir); + return { + ...overrides, + // ...dotenv + } + } + // TODO: fix return type once bit has a new stable version async run(context: AppContext): Promise { const depsResolver = context.getAspect(DependencyResolverAspect.id); @@ -165,6 +175,7 @@ export class AngularApp implements Application { this.generateTsConfig(bitCmps, appRootPath, appTsconfigPath, tsconfigPath, depsResolver, workspace); if (Number(VERSION.major) >= 16) { + const envVars = await this.getEnvFile('development', appRootPath, context.envVariables as any); await serveApplication({ angularOptions: { ...this.options.angularBuildOptions as ApplicationOptions, @@ -174,7 +185,10 @@ export class AngularApp implements Application { workspaceRoot: appRootPath, port, logger: logger, - tempFolder: tempFolder + tempFolder: tempFolder, + envVars: { + 'process.env': envVars + } }); } else { const devServerContext = this.getDevServerContext(context, appRootPath); @@ -206,6 +220,7 @@ export class AngularApp implements Application { this.generateTsConfig([capsule.component], appRootPath, appTsconfigPath, tsconfigPath, depsResolver, undefined, entryServer); if (!this.options.bundler && Number(VERSION.major) >= 16) { + const envVars = await this.getEnvFile('production', appRootPath, context.envVariables as any); await buildApplication({ angularOptions: { ...appOptions, @@ -216,7 +231,10 @@ export class AngularApp implements Application { workspaceRoot: context.capsule.path, logger: logger, tempFolder: tempFolder, - entryServer + entryServer, + envVars: { + 'process.env': envVars + } }); } else { let bundler: Bundler; diff --git a/angular/app-types/angular-app-type/application.bundler.ts b/angular/app-types/angular-app-type/application.bundler.ts index 902d2f8..89f648e 100644 --- a/angular/app-types/angular-app-type/application.bundler.ts +++ b/angular/app-types/angular-app-type/application.bundler.ts @@ -18,6 +18,7 @@ import { outputFileSync } from 'fs-extra'; // @ts-ignore import type { NitroConfig } from 'nitropack'; import { basename, extname, join, posix, relative, resolve } from 'path'; +import definePlugin from './plugins/define.plugin'; import { getIndexInputFile } from './utils'; export type BuildApplicationOptions = { @@ -28,6 +29,7 @@ export type BuildApplicationOptions = { logger: Logger; tempFolder: string; entryServer?: string; + envVars: any; } // TODO allow customizing this @@ -35,24 +37,22 @@ const BUILDER_NAME = '@angular-devkit/build-angular:application'; const CACHE_PATH = 'angular/cache'; export async function buildApplication(options: BuildApplicationOptions): Promise { - const { angularOptions: { tsConfig, ssr } } = options; - const isSsr = !!ssr && Number(VERSION.major) >= 17; - + const { angularOptions: { tsConfig, ssr, define }, envVars } = options; assert(tsConfig, 'tsConfig option is required'); - - if(isSsr) { + const isSsr = !!ssr && Number(VERSION.major) >= 17; + if (isSsr) { addEntryServer(options); } - const appOptions = getAppOptions(options, isSsr); const builderContext = getBuilderContext(options, appOptions); - const builderPlugins: any[] = []; + const codePlugins = [definePlugin({ ...envVars, ...define || {} })]; + const extensions: any = (Number(VERSION.major) >= 17 && Number(VERSION.minor) >= 1) ? { codePlugins } : []; for await (const result of buildApplicationInternal( appOptions as any, builderContext, { write: true }, - builderPlugins + extensions )) { if (!result.success && result.errors) { throw new Error(result.errors.map((err: any) => err.text).join('\n')); @@ -104,7 +104,7 @@ function getAppOptions(options: BuildApplicationOptions, isSsr: boolean): any { const normalizedBrowser = `./${ join(sourceRoot, 'main.ts') }`; const dedupedAssets = dedupPaths([posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]); - const dedupedStyles = dedupPaths([posix.join(sourceRoot, `styles.${angularOptions.inlineStyleLanguage}`), ...(angularOptions.styles ?? [])]); + const dedupedStyles = dedupPaths([posix.join(sourceRoot, `styles.${ angularOptions.inlineStyleLanguage }`), ...(angularOptions.styles ?? [])]); return { ...angularOptions, @@ -157,10 +157,7 @@ function getBuilderContext(options: BuildApplicationOptions, appOptions: Applica cli: { cache: { enabled: true, path: resolve(tempFolder, 'angular/cache') } } }); }, - addTeardown: (teardown: () => Promise | void) => { - teardown(); - return; - }, + addTeardown: () => {}, getBuilderNameForTarget: () => Promise.resolve(BUILDER_NAME), getTargetOptions: () => Promise.resolve(appOptions as any), validateOptions: () => Promise.resolve(appOptions as any) diff --git a/angular/app-types/angular-app-type/application.dev-server.ts b/angular/app-types/angular-app-type/application.dev-server.ts index 790cc59..bc7aa07 100644 --- a/angular/app-types/angular-app-type/application.dev-server.ts +++ b/angular/app-types/angular-app-type/application.dev-server.ts @@ -19,6 +19,7 @@ import type { NitroConfig } from 'nitropack'; import { join, posix, relative, resolve } from 'path'; // @ts-ignore import type { Connect } from 'vite'; +import definePlugin from './plugins/define.plugin'; export type ServeApplicationOptions = { angularOptions: Partial; @@ -27,6 +28,7 @@ export type ServeApplicationOptions = { logger: Logger; port: number; tempFolder: string; + envVars: any; } // TODO allow customizing this @@ -36,24 +38,21 @@ const BUILDER_NAME = '@angular-devkit/build-angular:application'; const CACHE_PATH = 'angular/cache'; export async function serveApplication(options: ServeApplicationOptions): Promise { - const { - angularOptions - } = options; - const isSsr = !!angularOptions.server && Number(VERSION.major) >= 17; - - assert(angularOptions.tsConfig, 'tsConfig option is required'); - - const appOptions = getAppOptions(options, isSsr); - const builderContext = getBuilderContext(options, appOptions); // intercept SIGINT and exit cleanly, otherwise the process will not exit properly on Ctrl+C process.on('SIGINT', () => process.exit(1)); + const { angularOptions: { server, tsConfig, define }, envVars } = options; + assert(tsConfig, 'tsConfig option is required'); + const isSsr = !!server && Number(VERSION.major) >= 17; + const appOptions = getAppOptions(options, isSsr); + const builderContext = getBuilderContext(options, appOptions); const devServerOptions = isSsr ? { + buildPlugins: [definePlugin({ ...envVars, ...define })], middleware: [await createNitroApiMiddleware(options)] } : undefined; // @ts-ignore only v17+ has 4 arguments, previous versions only have 3 - const res = await executeDevServerBuilder(appOptions, builderContext as any, undefined, devServerOptions).toPromise(); + await executeDevServerBuilder(appOptions, builderContext as any, undefined, devServerOptions).toPromise(); } function getAppOptions(options: ServeApplicationOptions, isSsr: boolean): ApplicationBuilderOptions & DevServerBuilderOptions { diff --git a/angular/app-types/angular-app-type/component.json b/angular/app-types/angular-app-type/component.json index 0b17bbb..56f5f39 100644 --- a/angular/app-types/angular-app-type/component.json +++ b/angular/app-types/angular-app-type/component.json @@ -18,6 +18,7 @@ "peerDependencies": { "@angular-devkit/build-angular": ">= 0.0.1", "@angular/cli": ">= 13.0.0", + "esbuild": ">= 0.14.0", "typescript": ">= 3.5.3" } } diff --git a/angular/app-types/angular-app-type/plugins/define.plugin.ts b/angular/app-types/angular-app-type/plugins/define.plugin.ts new file mode 100644 index 0000000..6dec684 --- /dev/null +++ b/angular/app-types/angular-app-type/plugins/define.plugin.ts @@ -0,0 +1,48 @@ +import { Plugin, PluginBuild } from 'esbuild'; + +export const stringifyDefine = (define: any) => { + return Object.entries(define).reduce((acc: any, [key, value]) => { + acc[key] = JSON.stringify(value); + return acc; + }, {}); +}; + +/** + * Pass environment variables to esbuild. + * @returns An esbuild plugin. + */ +export default function(defineValues = {}) { + // set variables on global so that they also work during ssr + const keys = Object.keys(defineValues); + keys.forEach((key: any) => { + // @ts-ignore + if (global[key]) { + throw new Error(`Define plugin: key ${ key } already exists on global`); + } else { + // @ts-ignore + global[key] = defineValues[key]; + } + }); + + const plugin: Plugin = { + name: 'env', + + setup(build: PluginBuild) { + const { platform, define = {} } = build.initialOptions; + if (platform === 'node') { + return; + } + + if (typeof defineValues !== 'string') { + defineValues = stringifyDefine(defineValues); + } + + build.initialOptions.define = { + ...defineValues, + ...define + }; + } + }; + + return plugin; +} diff --git a/angular/devkit/ng-compat/build-angular/builders/application.ts b/angular/devkit/ng-compat/build-angular/builders/application.ts index d537156..5919105 100644 --- a/angular/devkit/ng-compat/build-angular/builders/application.ts +++ b/angular/devkit/ng-compat/build-angular/builders/application.ts @@ -7,9 +7,11 @@ export let buildApplicationInternal = ( options: ApplicationBuilderOptions, context: BuilderContext & { signal?: AbortSignal; - }, infrastructureSettings?: { + }, + infrastructureSettings?: { write?: boolean; - }, plugins?: Plugin[] + }, + plugins?: Plugin[] | { codePlugins: Plugin[], indexHtmlTransformer: any } // @ts-ignore ) => AsyncIterable