@scania-nl/tegel-angular-extensions 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,8 @@
1
+ ![npm version](https://img.shields.io/npm/v/@scania-nl/tegel-angular-extensions)
2
+ ![angular](https://img.shields.io/badge/angular-19%2B-DD0031?logo=angular)
3
+ ![typescript](https://img.shields.io/badge/typescript-5%2B-3178C6?logo=typescript)
4
+ ![zod](https://img.shields.io/badge/zod-schema-3066BE)
5
+
1
6
  # @scania-nl/tegel-angular-extensions
2
7
 
3
8
  Angular services for working with the [Tegel Angular 17](https://www.npmjs.com/package/@scania/tegel-angular-17) component library.
@@ -7,38 +12,260 @@ Provides simple wrappers for toast and modal (TBC) functionality using Angular 1
7
12
 
8
13
  ## ✨ Features
9
14
 
10
- - Drop-in `ToastService` for displaying toasts
11
- - Zero boilerplate no Angular modules required
12
- - Fully typed and configurable via DI
13
- - Built for Angular 19+ standalone architecture
15
+ 1. **Runtime environment configuration**
16
+ - Schema-driven configuration and validation using Zod
17
+ - Deep-partial runtime overrides
18
+ - Runtime-required key enforcements
19
+ - Strong TypeScript inference
20
+ 2. **ToastService**
21
+ - Drop-in UI integrations for Tegel Angular
22
+ - Signal-based `ToastService` for displaying toasts
23
+ - Customizable toast appearance and behavior
24
+ 3. **Default Nginx config**
25
+
26
+ - Zero boilerplate - no Angular modules required
27
+ - Fully typed and configurable via DI
28
+ - Built for Angular 19+ standalone architecture
14
29
 
15
30
  ---
16
31
 
17
- ## 📦 Installation
32
+ # 📘 Table of Contents
33
+
34
+ 1. [Installation](#-installation)
35
+ 2. [Environment Configuration Overview](#-environment-configuration-overview)
36
+ 1. [Defining your schema with createEnvKit](#defining-your-schema-with-createenvkit)
37
+ 2. [Defining Static Environments (Dev/Prod)](#defining-static-environments-devprod)
38
+ 3. [Providing Runtime Configuration](#providing-runtime-configuration)
39
+ 4. [Using the ENV_CONFIG Token](#using-the-env_config-token)
40
+ 5. [Runtime Configuration Binaries](#runtime-configuration-binaries)
41
+ 1. [`docker-entrypoint.sh`](#docker-entrypointsh)
42
+ 2. [`extract-env-vars.sh`](#extract-env-varssh)
43
+ 3. [`nginx.conf`](#nginxconf)
44
+ 4. [Example Docker Commands](#example-docker-commands)
45
+ 3. [Toasts](#-toasts)
46
+ 1. [Quick Start](#-quick-start)
47
+ 1. [Add Providers](#add-providers)
48
+ 2. [Use in components](#use-in-components)
49
+ 2. [Configuration Options](#️-configuration-options)
50
+ 3. [ToastService API](#-toastservice-api)
51
+ 1. [ToastService Properties](#-toastservice-properties)
52
+ 2. [ToastService Methods](#-toastservice-methods)
53
+ 3. [Toast Lifecycle Hooks](#-toast-lifecycle-hooks)
54
+ 4. [Components & Directives](#-components--directives)
55
+ 1. [Components](#-components)
56
+ 1. [Footer](#footer-component-tae-footer)
57
+ 2. [Directives](#-directives)
58
+ 1. [Hard Refresh](#hard-refresh-directive-taehardrefresh)
59
+ 5. [Appendix](#appendix)
60
+ 1. [Dockerfile Example](#runtime-config-dockerfile-example)
61
+ 6. [License](#-license)
62
+
63
+ ---
64
+
65
+ # 📦 Installation
18
66
 
19
67
  ```bash
20
- npm install @scania-nl/tegel-angular-extensions @scania/tegel-angular-17
68
+ npm install @scania-nl/tegel-angular-extensions @scania/tegel-angular-17 @traversable/zod zod
21
69
  ```
22
70
 
23
- > Note: `@scania/tegel-angular-17` is a peer dependency and must be installed separately.
71
+ > Note: `@scania/tegel-angular-17`, `@traversable/zod`, and `zod` are peer dependencies and must be installed separately.
24
72
 
25
- The following peer dependencies should be included automatically when creating an Angular 19+ project:
73
+ When creating an Angular project, the following dependencies already should have been installed:
26
74
 
27
75
  ```json
28
76
  {
29
77
  "@angular/common": "^19.0.0",
30
78
  "@angular/core": "^19.0.0",
31
79
  "@angular/router": "^19.0.0",
32
- "rxjs": "~7.8.0"
80
+ "rxjs": ">=7.8.0"
33
81
  }
34
82
  ```
35
83
 
36
84
  ---
37
85
 
86
+ # 🌍 Environment Configuration Overview
87
+
88
+ The runtime-config system provides:
89
+
90
+ - Type-safe configuration via Zod schemas
91
+ - Static environment validation
92
+ - Runtime overrides via .env files (shell scripts included)
93
+ - Guaranteed config availability before app bootstrap
94
+
95
+ ## Defining your schema with createEnvKit
96
+
97
+ Create a local file: `src/environments/environment-config.ts`
98
+
99
+ ```ts
100
+ import { InjectionToken } from '@angular/core';
101
+ import { createEnvKit } from '@scania-nl/tegel-angular-extensions';
102
+ import { z } from 'zod';
103
+
104
+ // Create the EnvKit by defining a Zod-schema
105
+ export const EnvKit = createEnvKit({
106
+ schema: z
107
+ .object({
108
+ envType: z.enum(['dev', 'preprod', 'staging', 'prod']),
109
+ production: z.boolean(),
110
+ apiUrl: z.url(),
111
+ })
112
+ .strict(),
113
+ runtimeRequiredKeys: ['apiUrl'],
114
+ });
115
+
116
+ // Re-export for static env files
117
+ type EnvConfig = typeof EnvKit.types.EnvConfig;
118
+ export type StaticEnv = typeof EnvKit.types.StaticEnv;
119
+
120
+ // Injection Token for EnvConfig
121
+ export const ENV_CONFIG = new InjectionToken<EnvConfig>('ENV_CONFIG');
122
+ ```
123
+
124
+ &nbsp;
125
+
126
+ ## Defining Static Environments (Dev/Prod)
127
+
128
+ Initialize the environments using Nx:
129
+
130
+ ```bash
131
+ nx g environments
132
+ ```
133
+
134
+ This will create two files: `environment.ts` for the production environment configuration and `environment.development.ts` for the local development environment configuration.
135
+
136
+ `environment.development.ts`:
137
+
138
+ ```ts
139
+ import { StaticEnv } from './environment-config';
140
+
141
+ export const environment: StaticEnv = {
142
+ envType: 'dev',
143
+ production: false,
144
+ apiUrl: 'https://www.company.com/api/',
145
+ } satisfies StaticEnv;
146
+ ```
147
+
148
+ `environment.ts`:
149
+
150
+ ```ts
151
+ import { StaticEnv } from './environment-config';
152
+
153
+ export const environment: StaticEnv = {
154
+ envType: 'prod',
155
+ production: true,
156
+ // apiUrl: 'https://www.company.com/api/' // apiUrl cannot be defined here
157
+ } satisfies StaticEnv;
158
+ ```
159
+
160
+ &nbsp;
161
+
162
+ ## Providing Runtime Configuration
163
+
164
+ Add to `app.config.ts`:
165
+
166
+ ```ts
167
+ import { provideRuntimeConfig } from '@scania-nl/tegel-angular-extensions';
168
+ import { EnvKit, ENV_CONFIG } from '../environments/environment-config';
169
+ import { environment } from '../environments/environment';
170
+
171
+ export const appConfig: ApplicationConfig = {
172
+ providers: [
173
+ // ...
174
+ provideRuntimeConfig(EnvKit, environment, {
175
+ envPath: '/env/runtime.env', // Default
176
+ debug: !isDevMode(), // Defaults to false
177
+ stopOnError: true, // Default
178
+ token: ENV_CONFIG,
179
+ }),
180
+ ],
181
+ };
182
+ ```
183
+
184
+ &nbsp;
185
+
186
+ ## Using the ENV_CONFIG Token
187
+
188
+ ```ts
189
+ import { inject } from '@angular/core';
190
+ import { ENV_CONFIG } from '../environments/environment-config';
191
+
192
+ @Injectable()
193
+ export class ApiService {
194
+ private readonly envConfig = inject(ENV_CONFIG);
195
+
196
+ constructor() {
197
+ console.log('API base URL:', this.envConfig.apiUrl);
198
+ }
199
+ }
200
+ ```
201
+
202
+ &nbsp;
203
+ This setup involves several key steps:
204
+
205
+ - At application startup, the code loads `/env/runtime.env`, parses any overrides using Zod (via a deep-partial schema), and merges them with the static configuration.
206
+ - Required configuration keys are enforced **only** in production.
207
+ - The validated configuration is exposed through Angular DI using the `ENV_CONFIG` InjectionToken.
208
+ - The `environment` file is referenced directly here. Angular’s build process replaces the `development` environment with the `production` environment via file replacement based on the selected build configuration.
209
+
210
+ > The `/env/runtime.env` file must be generated by the container during startup.
211
+ > Shell script binaries to support this are included the package.
212
+
213
+ &nbsp;
214
+
215
+ ## Runtime Configuration Binaries
216
+
217
+ This package ships with two lightweight shell scripts used to generate a runtime configuration file inside the container. They enable true runtime configurability without rebuilding the Angular image. Additionally, the package contains a default `nginx.conf` optimized for Angular application. The files are located in the `/docker` directory.
218
+
219
+ ### `docker-entrypoint.sh`
220
+
221
+ - Entry point executed every time the container starts
222
+ - Calls the `extract-env-vars.sh` to generate a fresh `runtime.env`
223
+ - Lastly, executes the provided Dockerfile `CMD`
224
+
225
+ ### `extract-env-vars.sh`
226
+
227
+ - Reads all container environment variables matching a prefix (default: `NG__`)
228
+ - Strips the prefix and writes cleaned `KEY=VALUE` pairs to `runtime.env`
229
+ - Supports nested keys via ** (e.g., `NG**myFeature\_\_myThreshold`)
230
+ - Defaults output to `/usr/share/nginx/html/env/runtime.env`
231
+
232
+ ### `nginx.conf`
233
+
234
+ A default Nginx configuration optimized for Angular applications. It provides:
235
+
236
+ - Performance tuning for static file serving
237
+ - Browser caching of compiled assets
238
+ - Gzip compression where supported
239
+ - Automatic fallback to `index.html` for client-side routing
240
+
241
+ ### **Example Docker Commands**
242
+
243
+ ```Dockerfile
244
+ # Copy the shell scripts to /usr/local/bin
245
+ COPY --from=build /app/node_modules/@scania-nl/tegel-angular-extensions/docker/*.sh /usr/local/bin/
246
+ # Ensure they shell scripts are executable
247
+ RUN chmod +x /usr/local/bin/*
248
+ # Use the shared entrypoint from the @scania-nl/tegel-angular-extensions npm package
249
+ ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
250
+ # Start the nginx web server in the foreground and keep it running
251
+ CMD ["nginx", "-g", "daemon off;"]
252
+ ```
253
+
254
+ For a complete Dockerfile example with the shipped `nginx.conf` config, refer to [Runtime Config Dockerfile Example](#runtime-config-dockerfile-example)
255
+
256
+ &nbsp;
257
+
258
+ ---
259
+
260
+ # 🔔 Toasts
261
+
262
+ A lightweight, standalone toast system that integrates seamlessly with Tegel Angular. Provides configurable, signal-driven notifications for success, error, warning, and information messages.
263
+
38
264
  ## 🚀 Quick Start
39
265
 
40
- ### 1. Add Providers
41
- In your `app.config.ts`, specifiy the provider with `provideToast()`:
266
+ ### Add Providers
267
+
268
+ In your `app.config.ts`, specify the provider with `provideToast()`:
42
269
 
43
270
  ```ts
44
271
  import { provideToast } from '@scania-nl/tegel-angular-extensions';
@@ -46,19 +273,21 @@ import { provideToast } from '@scania-nl/tegel-angular-extensions';
46
273
  export const appConfig: ApplicationConfig = {
47
274
  providers: [
48
275
  provideToast({
49
- type: 'information', // Default toast type
50
- title: 'Notification', // Default title
51
- description: '', // Default description
52
- duration: 7500, // Auto-dismiss delay (ms)
53
- closeDuration: 300, // Fade-out animation duration (ms)
54
- closable: true, // Show a close button
276
+ type: 'information', // Default toast type
277
+ title: 'Notification', // Default title
278
+ description: '', // Default description
279
+ duration: 7500, // Auto-dismiss delay (ms)
280
+ closeDuration: 300, // Fade-out animation duration (ms)
281
+ closable: true, // Show a close button
55
282
  }),
56
283
  ],
57
284
  };
58
285
  ```
286
+
59
287
  > Note: The configuration is optional, all values shown above are the default settings.
60
288
 
61
- ### 2. Use in components
289
+ ### Use in components
290
+
62
291
  In any standalone component:
63
292
 
64
293
  ```ts
@@ -72,9 +301,9 @@ export class MyToastDemoComponent {
72
301
 
73
302
  showToast() {
74
303
  this.toastService.create({
75
- type: 'success',
76
- title: 'Hello Toast',
77
- description: 'Toast created successfully!'
304
+ type: 'success',
305
+ title: 'Hello Toast',
306
+ description: 'Toast created successfully!',
78
307
  });
79
308
  }
80
309
  }
@@ -82,6 +311,8 @@ export class MyToastDemoComponent {
82
311
 
83
312
  ---
84
313
 
314
+ &nbsp;
315
+
85
316
  ## ⚙️ Configuration Options
86
317
 
87
318
  You can configure the default appearance and behavior of toasts by passing a `ToastConfig` object to `provideToast()` in your `app.config.ts`.
@@ -101,13 +332,15 @@ All options are optional. Defaults will be applied if values are not provided.
101
332
 
102
333
  ---
103
334
 
335
+ &nbsp;
336
+
104
337
  ## 🧩 ToastService API
105
338
 
106
339
  The `ToastService` provides a signal-based API to create, manage, and dismiss toast notifications in Angular standalone apps. It is automatically available after registering `provideToast()` in your `app.config.ts`.
107
340
 
108
341
  ---
109
342
 
110
- ### 📦 Properties
343
+ ### 📦 ToastService Properties
111
344
 
112
345
  | Property | Type | Description |
113
346
  | -------------- | ----------------- | ----------------------------------------------------- |
@@ -116,7 +349,7 @@ The `ToastService` provides a signal-based API to create, manage, and dismiss to
116
349
 
117
350
  ---
118
351
 
119
- ### 🔧 Methods
352
+ ### 🔧 ToastService Methods
120
353
 
121
354
  #### `create(toastOptions: Partial<ToastOptions>): number`
122
355
 
@@ -135,7 +368,7 @@ toastService.create({
135
368
 
136
369
  Returns the unique toast ID.
137
370
 
138
- ---
371
+ &nbsp;
139
372
 
140
373
  #### Convenience Methods
141
374
 
@@ -148,13 +381,13 @@ toastService.warning({ title: 'Heads up!' });
148
381
  toastService.info({ title: 'FYI' });
149
382
  ```
150
383
 
151
- ---
384
+ &nbsp;
152
385
 
153
386
  #### `getToast(id: number): Toast | undefined`
154
387
 
155
388
  Gets a toast by its ID.
156
389
 
157
- ---
390
+ &nbsp;
158
391
 
159
392
  #### `createRandomToast(props?: Partial<ToastOptions>): number`
160
393
 
@@ -164,25 +397,25 @@ Creates a random toast with random type and title. Useful for testing. Returns t
164
397
  toastService.createRandomToast();
165
398
  ```
166
399
 
167
- ---
400
+ &nbsp;
168
401
 
169
402
  #### `close(id: number): void`
170
403
 
171
404
  Triggers the fade-out animation and schedules removal.
172
405
 
173
- ---
406
+ &nbsp;
174
407
 
175
408
  #### `closeAll(): void`
176
409
 
177
410
  Closes all currently open toasts.
178
411
 
179
- ---
412
+ &nbsp;
180
413
 
181
414
  #### `remove(id: number): void`
182
415
 
183
416
  Immediately removes a toast (no animation).
184
417
 
185
- ---
418
+ &nbsp;
186
419
 
187
420
  #### `removeAll(): void`
188
421
 
@@ -206,12 +439,136 @@ Example:
206
439
  toastService.success({
207
440
  title: 'Logged out',
208
441
  duration: 5000,
209
- onRemoved: (toast) => console.log(`Toast ${toast.id} removed`)
442
+ onRemoved: (toast) => console.log(`Toast ${toast.id} removed`),
210
443
  });
211
444
  ```
212
445
 
213
446
  ---
214
447
 
215
- ## 📄 License
448
+ &nbsp;
449
+
450
+ # 🧩 Components & Directives
451
+
452
+ This library includes a set of standalone UI components and utility directives, all prefixed with **`tae`**, designed to extend and complement the Tegel Angular ecosystem. Each piece is lightweight, fully typed, and easy to import into any Angular 19+ application.
453
+ &nbsp;
454
+
455
+ ## 🧱 Components
456
+
457
+ ### Footer Component (`tae-footer`)
458
+
459
+ `TaeFooterComponent` is an enhanced footer based on the Tegel `TdsFooterComponent`. It preserves the same visual appearance while adding two key improvements:
460
+
461
+ - A **compact “small” variant** for constrained layouts.
462
+ - An optional **version display**, allowing applications to show their build or release version directly in the footer.
463
+
464
+ This makes it ideal for full-viewport layouts that benefit from space efficiency and clear version visibility.
465
+
466
+ **Inputs:**
467
+
468
+ | Input | Type | Default | Description |
469
+ | ------- | --------------------- | ------- | ------------------------------------------------------------------------------------------ |
470
+ | variant | `'normal' \| 'small'` | normal | Layout style of the footer. `'small'` produces a more compact version. |
471
+ | version | `string \| undefined` | — | Optional application version string. If provided, it is displayed left of the Scania logo. |
472
+
473
+ **Example:**
474
+
475
+ ```html
476
+ <tae-footer variant="small" version="v1.0.0" />
477
+ ```
478
+
479
+ &nbsp;
480
+
481
+ ## ⚡ Directives
482
+
483
+ ### Hard Refresh Directive (`taeHardRefresh`)
484
+
485
+ The `HardRefreshDirective` provides a small UX shortcut: it performs a **full page reload** when the host element is clicked **N times in rapid succession**, where each click must occur within `clickWindowMs` milliseconds of the previous one.
486
+
487
+ This is especially useful for hard-reloading tablet or mobile applications which are locked in full-screen mode, and thus have no browser buttons like refresh.
488
+
489
+ **Inputs:**
490
+
491
+ | Input | Type | Default | Description |
492
+ | -------------- | ------ | ------- | ---------------------------------------------------------------------- |
493
+ | clicksRequired | number | 3 | Number of clicks required within the window to trigger a hard refresh. |
494
+ | clickWindowMs | number | 500 | Time window in milliseconds between two subsequent clicks. |
495
+
496
+ **Example:**
497
+
498
+ ```html
499
+ <tds-header-brand-symbol
500
+ taeHardRefresh
501
+ [clicksRequired]="3"
502
+ [clickWindowMs]="500"
503
+ >
504
+ <a aria-label="Scania - red gryphon on blue shield"></a>
505
+ </tds-header-brand-symbol>
506
+ ```
507
+
508
+ ---
509
+
510
+ &nbsp;
511
+
512
+ # Appendix
513
+
514
+ ## Runtime Config Dockerfile Example
515
+
516
+ ```Dockerfile
517
+ ARG NODE_VERSION=25-alpine
518
+ ARG NGINX_VERSION=1.29.3-alpine
519
+
520
+ # Stage 1: Build the Angular Application
521
+ FROM node:${NODE_VERSION} AS build
522
+
523
+ WORKDIR /app
524
+
525
+ # Copy package-related files
526
+ COPY package*.json ./
527
+
528
+ # Install project dependencies using npm ci (ensures a clean, reproducible install)
529
+ RUN --mount=type=cache,target=/root/.npm npm ci
530
+
531
+ # Copy the rest of the application source code into the container
532
+ COPY . .
533
+
534
+ # Build the Angular application with the specified app version
535
+ RUN npm run build
536
+
537
+
538
+ # Stage 2: Prepare Nginx to Serve Static Files
539
+ FROM nginx:${NGINX_VERSION}
540
+
541
+ # Copy the static build output from the build stage to Nginx's default HTML serving directory
542
+ COPY --from=build /app/dist/*/browser /usr/share/nginx/html
543
+
544
+ # Copy the shell scripts from the @scania-nl/tegel-angular-extensions npm package
545
+ COPY --from=build /app/node_modules/@scania-nl/tegel-angular-extensions/docker/*.sh /usr/local/bin/
546
+ # Copy the custom Nginx configuration file from the @scania-nl/tegel-angular-extensions npm package
547
+ COPY --from=build /app/node_modules/@scania-nl/tegel-angular-extensions/docker/nginx.conf /etc/nginx/nginx.conf
548
+
549
+ # Ensure the shell scripts are executable
550
+ RUN chmod +x /usr/local/bin/*
551
+
552
+ # Expose ports 80 and 443 to allow HTTP and HTTPS traffic
553
+ EXPOSE 80 443
554
+
555
+ # Use the shared entrypoint from the @scania-nl/tegel-angular-extensions npm package
556
+ ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
557
+
558
+ # Start the nginx web server with custom config in the foreground and keep it running
559
+ CMD ["nginx", "-c", "/etc/nginx/nginx.conf", "-g", "daemon off;"]
560
+ ```
561
+
562
+ ---
563
+
564
+ # 📄 License
565
+
566
+ Copyright 2025 Scania CV AB.
567
+
568
+ All files are available under the MIT license. The Scania brand identity, logos and photographs found in this repository are copyrighted Scania CV AB and are not available on an open source basis or to be used as examples or in any other way, if not specifically ordered by Scania CV AB.
569
+
570
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
571
+
572
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
216
573
 
217
- All CSS, HTML and JS code are available under the MIT license. The Scania brand identity, logos and photographs found in this repository are copyrighted Scania CV AB and are not available on an open source basis or to be used as examples or in any other way, if not specifically ordered by Scania CV AB.
574
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,13 @@
1
+ #!/bin/sh
2
+
3
+ ENV_OUTPUT_PATH="/usr/share/nginx/html/env/runtime.env"
4
+
5
+ rm -f "$ENV_OUTPUT_PATH" # Remove the runtime config if it exists
6
+
7
+ # Generate the runtime config from env vars
8
+ if ! /usr/local/bin/extract-env-vars.sh "" "$ENV_OUTPUT_PATH"; then
9
+ echo "Failed to extract runtime environment variables, continuing with defaults..."
10
+ fi
11
+
12
+ # Then start Nginx
13
+ exec "$@"
@@ -0,0 +1,114 @@
1
+ #!/bin/sh
2
+
3
+ # --------------
4
+ # Configuration
5
+ # --------------
6
+
7
+ PREFIX="${1:-NG__}" # First argument = prefix, defaults to NG__
8
+ OUTPUT_FILE="${2:-./runtime.env}" # Second argument = output path, defaults to current dir
9
+ shift 2 # Remaining arguments = manual test vars like NG__key=value
10
+
11
+ OUTPUT_DIR=$(dirname "$OUTPUT_FILE") # Output directory
12
+ TMP_FILE="/tmp/runtime_env_$$.tmp" # Temp file for writing
13
+ : > "$TMP_FILE" # Create/empty the temp file
14
+
15
+ # --------------
16
+ # Logging Helpers
17
+ # --------------
18
+
19
+ log_info() { echo "[INFO] $*"; }
20
+ log_warn() { echo "[WARN] $*"; }
21
+ log_debug() { echo "[DEBUG] $*"; }
22
+
23
+ # --------------
24
+ # Validation
25
+ # --------------
26
+
27
+ validate_var() {
28
+ raw="$1"
29
+ key="${raw%%=*}"
30
+
31
+ # Must start with prefix
32
+ case "$key" in
33
+ "$PREFIX") return 1 ;; # only prefix, no key
34
+ "$PREFIX"*) ;;
35
+ *) return 1 ;;
36
+ esac
37
+
38
+ # Strip prefix
39
+ trimmed="${key#$PREFIX}"
40
+
41
+ # Key must not start with a digit
42
+ first_segment=$(printf '%s' "$trimmed" | cut -d'_' -f1)
43
+ case "$first_segment" in
44
+ [0-9]*)
45
+ return 1 ;;
46
+ esac
47
+
48
+ return 0
49
+ }
50
+
51
+ strip_prefix() {
52
+ printf '%s\n' "$1" | sed "s/^$PREFIX//"
53
+ }
54
+
55
+ # --------------
56
+ # Extract ENV vars
57
+ # --------------
58
+
59
+ log_info "Using prefix: '$PREFIX'"
60
+ log_info "Output file: $OUTPUT_FILE"
61
+
62
+ env | grep "^$PREFIX" | while IFS= read -r line; do
63
+ if validate_var "$line"; then
64
+ key="${line%%=*}"
65
+ val="${line#*=}"
66
+ clean_key=$(strip_prefix "$key")
67
+ printf '%s=%s\n' "$clean_key" "$val" >> "$TMP_FILE"
68
+ log_debug "Added from environment: $clean_key=$val"
69
+ else
70
+ log_warn "Skipped invalid env var: $line"
71
+ fi
72
+ done
73
+
74
+ # --------------
75
+ # Manual vars
76
+ # --------------
77
+
78
+ for var in "$@"; do
79
+ if validate_var "$var"; then
80
+ key="${var%%=*}"
81
+ val="${var#*=}"
82
+ clean_key=$(strip_prefix "$key")
83
+ printf '%s=%s\n' "$clean_key" "$val" >> "$TMP_FILE"
84
+ log_debug "Added from CLI: $clean_key=$val"
85
+ else
86
+ log_warn "Skipped invalid manual var: $var"
87
+ fi
88
+ done
89
+
90
+ # --------------
91
+ # Output
92
+ # --------------
93
+ # Ensure the output directory exists
94
+ if [ ! -d "$OUTPUT_DIR" ]; then
95
+ log_info "Creating output directory: $OUTPUT_DIR"
96
+ if ! mkdir -p "$OUTPUT_DIR"; then
97
+ log_warn "Failed to create directory '$OUTPUT_DIR'"
98
+ rm -f "$TMP_FILE"
99
+ exit 1
100
+ fi
101
+ fi
102
+
103
+ if [ -s "$TMP_FILE" ]; then
104
+ if mv "$TMP_FILE" "$OUTPUT_FILE"; then
105
+ log_info "Wrote cleaned env vars to $OUTPUT_FILE"
106
+ else
107
+ log_warn "Failed to move temp file to $OUTPUT_FILE"
108
+ rm -f "$TMP_FILE"
109
+ exit 1
110
+ fi
111
+ else
112
+ rm -f "$TMP_FILE"
113
+ log_warn "No valid env vars found with prefix '$PREFIX'. Skipping output."
114
+ fi