Skip to content

Commit 357cdf2

Browse files
committed
feat: support define and context env variables for apps
1 parent 28e37d6 commit 357cdf2

File tree

7 files changed

+98
-27
lines changed

7 files changed

+98
-27
lines changed

angular/app-types/angular-app-type/angular.application.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,16 @@ export class AngularApp implements Application {
146146
return context as any as EnvContext;
147147
}
148148

149+
private async getEnvFile(mode: string, rootDir: string, overrides?: Record<string, string>) {
150+
// TODO: enable this one we have ESM envs, otherwise we get a warning message about loading the deprecated CJS build of Vite
151+
// const vite = await loadEsmModule('vite');
152+
// const dotenv = vite.loadEnv(mode, rootDir);
153+
return {
154+
...overrides,
155+
// ...dotenv
156+
}
157+
}
158+
149159
// TODO: fix return type once bit has a new stable version
150160
async run(context: AppContext): Promise<ApplicationInstance> {
151161
const depsResolver = context.getAspect<DependencyResolverMain>(DependencyResolverAspect.id);
@@ -165,6 +175,7 @@ export class AngularApp implements Application {
165175
this.generateTsConfig(bitCmps, appRootPath, appTsconfigPath, tsconfigPath, depsResolver, workspace);
166176

167177
if (Number(VERSION.major) >= 16) {
178+
const envVars = await this.getEnvFile('development', appRootPath, context.envVariables as any);
168179
await serveApplication({
169180
angularOptions: {
170181
...this.options.angularBuildOptions as ApplicationOptions,
@@ -174,7 +185,10 @@ export class AngularApp implements Application {
174185
workspaceRoot: appRootPath,
175186
port,
176187
logger: logger,
177-
tempFolder: tempFolder
188+
tempFolder: tempFolder,
189+
envVars: {
190+
'process.env': envVars
191+
}
178192
});
179193
} else {
180194
const devServerContext = this.getDevServerContext(context, appRootPath);
@@ -206,6 +220,7 @@ export class AngularApp implements Application {
206220
this.generateTsConfig([capsule.component], appRootPath, appTsconfigPath, tsconfigPath, depsResolver, undefined, entryServer);
207221

208222
if (!this.options.bundler && Number(VERSION.major) >= 16) {
223+
const envVars = await this.getEnvFile('production', appRootPath, context.envVariables as any);
209224
await buildApplication({
210225
angularOptions: {
211226
...appOptions,
@@ -216,7 +231,10 @@ export class AngularApp implements Application {
216231
workspaceRoot: context.capsule.path,
217232
logger: logger,
218233
tempFolder: tempFolder,
219-
entryServer
234+
entryServer,
235+
envVars: {
236+
'process.env': envVars
237+
}
220238
});
221239
} else {
222240
let bundler: Bundler;

angular/app-types/angular-app-type/application.bundler.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { outputFileSync } from 'fs-extra';
1818
// @ts-ignore
1919
import type { NitroConfig } from 'nitropack';
2020
import { basename, extname, join, posix, relative, resolve } from 'path';
21+
import definePlugin from './plugins/define.plugin';
2122
import { getIndexInputFile } from './utils';
2223

2324
export type BuildApplicationOptions = {
@@ -28,31 +29,30 @@ export type BuildApplicationOptions = {
2829
logger: Logger;
2930
tempFolder: string;
3031
entryServer?: string;
32+
envVars: any;
3133
}
3234

3335
// TODO allow customizing this
3436
const BUILDER_NAME = '@angular-devkit/build-angular:application';
3537
const CACHE_PATH = 'angular/cache';
3638

3739
export async function buildApplication(options: BuildApplicationOptions): Promise<void> {
38-
const { angularOptions: { tsConfig, ssr } } = options;
39-
const isSsr = !!ssr && Number(VERSION.major) >= 17;
40-
40+
const { angularOptions: { tsConfig, ssr, define }, envVars } = options;
4141
assert(tsConfig, 'tsConfig option is required');
42-
43-
if(isSsr) {
42+
const isSsr = !!ssr && Number(VERSION.major) >= 17;
43+
if (isSsr) {
4444
addEntryServer(options);
4545
}
46-
4746
const appOptions = getAppOptions(options, isSsr);
4847
const builderContext = getBuilderContext(options, appOptions);
49-
const builderPlugins: any[] = [];
48+
const codePlugins = [definePlugin({ ...envVars, ...define || {} })];
49+
const extensions: any = (Number(VERSION.major) >= 17 && Number(VERSION.minor) >= 1) ? { codePlugins } : [];
5050

5151
for await (const result of buildApplicationInternal(
5252
appOptions as any,
5353
builderContext,
5454
{ write: true },
55-
builderPlugins
55+
extensions
5656
)) {
5757
if (!result.success && result.errors) {
5858
throw new Error(result.errors.map((err: any) => err.text).join('\n'));
@@ -104,7 +104,7 @@ function getAppOptions(options: BuildApplicationOptions, isSsr: boolean): any {
104104
const normalizedBrowser = `./${ join(sourceRoot, 'main.ts') }`;
105105

106106
const dedupedAssets = dedupPaths([posix.join(sourceRoot, `assets/**/*`), ...(angularOptions.assets ?? [])]);
107-
const dedupedStyles = dedupPaths([posix.join(sourceRoot, `styles.${angularOptions.inlineStyleLanguage}`), ...(angularOptions.styles ?? [])]);
107+
const dedupedStyles = dedupPaths([posix.join(sourceRoot, `styles.${ angularOptions.inlineStyleLanguage }`), ...(angularOptions.styles ?? [])]);
108108

109109
return {
110110
...angularOptions,
@@ -157,10 +157,7 @@ function getBuilderContext(options: BuildApplicationOptions, appOptions: Applica
157157
cli: { cache: { enabled: true, path: resolve(tempFolder, 'angular/cache') } }
158158
});
159159
},
160-
addTeardown: (teardown: () => Promise<void> | void) => {
161-
teardown();
162-
return;
163-
},
160+
addTeardown: () => {},
164161
getBuilderNameForTarget: () => Promise.resolve(BUILDER_NAME),
165162
getTargetOptions: () => Promise.resolve(appOptions as any),
166163
validateOptions: () => Promise.resolve(appOptions as any)

angular/app-types/angular-app-type/application.dev-server.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { NitroConfig } from 'nitropack';
1919
import { join, posix, relative, resolve } from 'path';
2020
// @ts-ignore
2121
import type { Connect } from 'vite';
22+
import definePlugin from './plugins/define.plugin';
2223

2324
export type ServeApplicationOptions = {
2425
angularOptions: Partial<ApplicationBuilderOptions & DevServerBuilderOptions>;
@@ -27,6 +28,7 @@ export type ServeApplicationOptions = {
2728
logger: Logger;
2829
port: number;
2930
tempFolder: string;
31+
envVars: any;
3032
}
3133

3234
// TODO allow customizing this
@@ -36,24 +38,21 @@ const BUILDER_NAME = '@angular-devkit/build-angular:application';
3638
const CACHE_PATH = 'angular/cache';
3739

3840
export async function serveApplication(options: ServeApplicationOptions): Promise<void> {
39-
const {
40-
angularOptions
41-
} = options;
42-
const isSsr = !!angularOptions.server && Number(VERSION.major) >= 17;
43-
44-
assert(angularOptions.tsConfig, 'tsConfig option is required');
45-
46-
const appOptions = getAppOptions(options, isSsr);
47-
const builderContext = getBuilderContext(options, appOptions);
4841
// intercept SIGINT and exit cleanly, otherwise the process will not exit properly on Ctrl+C
4942
process.on('SIGINT', () => process.exit(1));
5043

44+
const { angularOptions: { server, tsConfig, define }, envVars } = options;
45+
assert(tsConfig, 'tsConfig option is required');
46+
const isSsr = !!server && Number(VERSION.major) >= 17;
47+
const appOptions = getAppOptions(options, isSsr);
48+
const builderContext = getBuilderContext(options, appOptions);
5149
const devServerOptions = isSsr ? {
50+
buildPlugins: [definePlugin({ ...envVars, ...define })],
5251
middleware: [await createNitroApiMiddleware(options)]
5352
} : undefined;
5453

5554
// @ts-ignore only v17+ has 4 arguments, previous versions only have 3
56-
const res = await executeDevServerBuilder(appOptions, builderContext as any, undefined, devServerOptions).toPromise();
55+
await executeDevServerBuilder(appOptions, builderContext as any, undefined, devServerOptions).toPromise();
5756
}
5857

5958
function getAppOptions(options: ServeApplicationOptions, isSsr: boolean): ApplicationBuilderOptions & DevServerBuilderOptions {

angular/app-types/angular-app-type/component.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"peerDependencies": {
1919
"@angular-devkit/build-angular": ">= 0.0.1",
2020
"@angular/cli": ">= 13.0.0",
21+
"esbuild": ">= 0.14.0",
2122
"typescript": ">= 3.5.3"
2223
}
2324
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Plugin, PluginBuild } from 'esbuild';
2+
3+
export const stringifyDefine = (define: any) => {
4+
return Object.entries(define).reduce((acc: any, [key, value]) => {
5+
acc[key] = JSON.stringify(value);
6+
return acc;
7+
}, {});
8+
};
9+
10+
/**
11+
* Pass environment variables to esbuild.
12+
* @returns An esbuild plugin.
13+
*/
14+
export default function(defineValues = {}) {
15+
// set variables on global so that they also work during ssr
16+
const keys = Object.keys(defineValues);
17+
keys.forEach((key: any) => {
18+
// @ts-ignore
19+
if (global[key]) {
20+
throw new Error(`Define plugin: key ${ key } already exists on global`);
21+
} else {
22+
// @ts-ignore
23+
global[key] = defineValues[key];
24+
}
25+
});
26+
27+
const plugin: Plugin = {
28+
name: 'env',
29+
30+
setup(build: PluginBuild) {
31+
const { platform, define = {} } = build.initialOptions;
32+
if (platform === 'node') {
33+
return;
34+
}
35+
36+
if (typeof defineValues !== 'string') {
37+
defineValues = stringifyDefine(defineValues);
38+
}
39+
40+
build.initialOptions.define = {
41+
...defineValues,
42+
...define
43+
};
44+
}
45+
};
46+
47+
return plugin;
48+
}

angular/devkit/ng-compat/build-angular/builders/application.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ export let buildApplicationInternal = (
77
options: ApplicationBuilderOptions,
88
context: BuilderContext & {
99
signal?: AbortSignal;
10-
}, infrastructureSettings?: {
10+
},
11+
infrastructureSettings?: {
1112
write?: boolean;
12-
}, plugins?: Plugin[]
13+
},
14+
plugins?: Plugin[] | { codePlugins: Plugin[], indexHtmlTransformer: any }
1315
// @ts-ignore
1416
) => AsyncIterable<BuilderOutput & {
1517
outputFiles?: OutputFile[];

angular/devkit/ng-compat/build-angular/schemas/application.schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export interface ApplicationBuilderOptions {
4040
* Delete the output path before building.
4141
*/
4242
deleteOutputPath?: boolean;
43+
/**
44+
* Defines global identifiers that will be replaced with a specified constant value when found in any JavaScript or TypeScript code including libraries.
45+
* The value will be used directly.
46+
* String values must be put in quotes. Identifiers within Angular metadata such as Component Decorators will not be replaced.
47+
*/
48+
define?: any;
4349
/**
4450
* Exclude the listed external dependencies from being bundled into the bundle. Instead, the
4551
* created bundle relies on these dependencies to be available during runtime.

0 commit comments

Comments
 (0)