@rivinia/env 0.3.0 → 0.3.2

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
@@ -3,50 +3,49 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@rivinia/env.svg)](https://www.npmjs.com/package/@rivinia/env)
4
4
  [![license](https://img.shields.io/npm/l/@rivinia/env.svg)](https://www.npmjs.com/package/@rivinia/env)
5
5
 
6
- Typed application environments for Angular: dev/staging/prod file layout, a
7
- `BaseEnvironment` contract, and an `ng-add` schematic that scaffolds the
8
- files and wires the build configurations. Zero runtime code — environments are
9
- build-time constants; this package ships types plus setup tooling only.
6
+ `@rivinia/env` provides the `BaseEnvironment` TypeScript contract and an
7
+ Angular CLI `ng add` schematic. It does not read environment values at
8
+ runtime: Angular's build file replacements select a statically imported
9
+ environment file.
10
10
 
11
- - Version: **0.3.0**
12
- - Requires: nothing at runtime (types only). Schematic runs under Angular CLI 22+.
11
+ - Package version: **0.3.2**
12
+ - No runtime providers or services; the package supplies a type and CLI tooling.
13
+ - The generated model imports `BaseEnvironment` from this package.
13
14
 
14
- ## Install
15
+ ## Install and scaffold
16
+
17
+ From an Angular CLI workspace:
15
18
 
16
19
  ```bash
17
20
  npm install @rivinia/env
18
- ng add @rivinia/env # multi-select: dev / stage / prod
21
+ ng add @rivinia/env
19
22
  ```
20
23
 
21
- Interactive runs ask which environments to scaffold (checkbox, all checked by
22
- default); non-interactive runs take all. From flags, comma-separate (short
23
- aliases work: `dev`, `prod`, `stag`):
24
+ The schematic prompts for environments when interactive; the default and
25
+ non-interactive selection is all three (`dev`, `stage`, `prod`). Select only
26
+ what you need with a comma-separated option:
24
27
 
25
28
  ```bash
26
29
  ng add @rivinia/env --environments stage,prod
27
30
  ```
28
31
 
29
- Only the selection is scaffolded and wired (model + base file always — the
30
- base is the replacement source). Each wired configuration gets its output
31
- directory (`builds/dev`, `builds/stag`, `builds/prod`) unless it already
32
- defines one, and `/builds` is appended to `.gitignore` once. Re-runs never
33
- overwrite files or duplicate configuration.
32
+ Accepted names are `dev`, `stage` (or `stag`), and `prod`. `--project <name>`
33
+ selects a project in a multi-project workspace; otherwise it uses
34
+ `defaultProject` from `angular.json`, falling back to the first project.
35
+ The schematic uses that project's `sourceRoot` (or `<root>/src`, then `src`).
34
36
 
35
- `ng add` creates (never overwrites) `src/environments/`:
37
+ The model and development base file are always scaffolded. The selected
38
+ environment files are created only if they do not already exist:
36
39
 
40
+ ```text
41
+ src/environments/
42
+ environment.model.ts # AppEnvironment extends BaseEnvironment
43
+ environment.ts # development defaults; not replaced by the CLI
44
+ environment.staging.ts # only when stage is selected
45
+ environment.prod.ts # only when prod is selected
37
46
  ```
38
- environment.model.ts # AppEnvironment extends BaseEnvironment — extend it
39
- environment.ts # dev defaults (used as-is by `dev`)
40
- environment.staging.ts # stage values (production: true)
41
- environment.prod.ts # prod values
42
- ```
43
-
44
- and wires `angular.json`: `production` gains its replacement if missing, and a
45
- `staging` configuration (copied from `production`) is created if absent.
46
- `development` intentionally gets no replacement (CLI standard). The app version
47
- is snapshotted from your `package.json` into each file (`0.0.0` fallback).
48
47
 
49
- ## Usage pattern (app-owned)
48
+ The generated contract and values have this shape:
50
49
 
51
50
  ```ts
52
51
  // environment.model.ts
@@ -55,14 +54,85 @@ import type { BaseEnvironment } from '@rivinia/env';
55
54
  export interface AppEnvironment extends BaseEnvironment {
56
55
  readonly apiUrl: string;
57
56
  }
57
+ ```
58
58
 
59
- // anywhere:
59
+ ```ts
60
+ // environment.ts (generated development defaults)
61
+ import type { AppEnvironment } from './environment.model';
62
+
63
+ export const environment: AppEnvironment = {
64
+ production: false,
65
+ version: '1.2.3',
66
+ apiUrl: 'http://localhost:3000/api',
67
+ };
68
+ ```
69
+
70
+ Staging and production files have the same fields, with `production: true`;
71
+ their generated API URLs are `https://staging.example.com/api` and
72
+ `https://api.example.com/api`. Replace these example URLs and any other
73
+ generated values with the app's real configuration.
74
+
75
+ The schematic creates the short `dev`, `stag`, and `prod` build configurations
76
+ for the selected environments, copying settings from the corresponding
77
+ `development`, `staging`, or `production` configuration when one exists.
78
+ Otherwise, it creates the short configuration with the required environment
79
+ replacement when needed (`dev` uses the base file directly). It also wires
80
+ existing legacy keys when present: `development`
81
+ uses `environment.ts`, `staging` replaces it with
82
+ `environment.staging.ts`, and `production` replaces it with
83
+ `environment.prod.ts`. The short names are the recommended CLI commands; the
84
+ legacy names remain available for existing scripts.
85
+
86
+ For selected configurations that exist, it sets `outputPath` to
87
+ `builds/dev`, `builds/stag`, or `builds/prod` only if no output path is
88
+ already configured. If that adds an output path and a root `.gitignore`
89
+ exists, `/builds` is appended once. Existing output paths and files are never
90
+ overwritten; repeated runs do not duplicate replacements.
91
+
92
+ The version in generated files is copied from the workspace-root
93
+ `package.json` when each file is first created (`0.0.0` if unavailable).
94
+ Since the schematic never overwrites existing files, update `version` in
95
+ those environment files manually when releasing a new app version; rerunning
96
+ `ng add` will not refresh it.
97
+
98
+ ## Use the selected environment
99
+
100
+ Always import the base path from the app, not a stage/prod-specific file:
101
+
102
+ ```ts
103
+ // src/app/trip.service.ts
104
+ import { Service } from '@angular/core';
60
105
  import { environment } from '../environments/environment';
61
- fetch(`${environment.apiUrl}/trips`);
62
- console.log(environment.version); // e.g. footer, about page, cache keys
106
+
107
+ @Service()
108
+ export class TripService {
109
+ readonly apiUrl = environment.apiUrl;
110
+ }
63
111
  ```
64
112
 
65
- Re-run the schematic (or edit by hand) when the app version changes.
113
+ After `ng add @rivinia/env`, build with the short configuration names:
114
+
115
+ ```bash
116
+ ng build --configuration dev
117
+ ng build --configuration stag
118
+ ng build --configuration prod
119
+ ```
120
+
121
+ These are actual keys in `angular.json`, not Angular CLI aliases. If you
122
+ maintain custom configurations, inspect the selected project's
123
+ `architect.build.configurations` and ensure each key has the intended
124
+ `fileReplacements`; `ng build --configuration` uses the key exactly as written.
125
+
126
+ The generated files are compile-time configuration, not secrets: values
127
+ included in a browser build are visible to its users. `BaseEnvironment`
128
+ requires only readonly `production: boolean` and `version: string`; extend
129
+ AppEnvironment with the fields your app needs.
130
+
131
+ ## Public API
132
+
133
+ | Export | Kind | Purpose |
134
+ | ----------------- | ---- | ---------------------------------------------------------------------- |
135
+ | `BaseEnvironment` | type | Required `production` and `version` fields for app environment models. |
66
136
 
67
137
  ## License
68
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/env",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Typed application environments for Angular (dev/staging/prod) with an ng-add schematic.",
5
5
  "keywords": [
6
6
  "angular",
@@ -28,8 +28,14 @@ const OUTPUT_DIR = {
28
28
  stage: 'builds/stag',
29
29
  prod: 'builds/prod',
30
30
  };
31
- /** Maps an environment name to its Angular CLI configuration name. */
31
+ /** Public short configuration names used by `ng build --configuration`. */
32
32
  const CONFIG_NAME = {
33
+ dev: 'dev',
34
+ stage: 'stag',
35
+ prod: 'prod',
36
+ };
37
+ /** Existing long configuration names remain wired for compatibility. */
38
+ const LEGACY_CONFIG_NAME = {
33
39
  dev: 'development',
34
40
  stage: 'staging',
35
41
  prod: 'production',
@@ -38,11 +44,11 @@ const fileDefs = (sourceRoot, selected) => {
38
44
  const files = [
39
45
  { name: `${sourceRoot}/environments/environment.model.ts`, content: () => MODEL_FILE },
40
46
  // The base file is always scaffolded: it is the `replace` source for
41
- // prod/staging swaps (and what `development` uses directly).
47
+ // prod/stag swaps (and what dev uses directly).
42
48
  {
43
49
  name: `${sourceRoot}/environments/environment.ts`,
44
50
  content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
45
- `/** Development defaults. Used as-is by the \`development\` configuration. */\n` +
51
+ `/** Development defaults. Used as-is by the \`dev\` configuration. */\n` +
46
52
  `export const environment: AppEnvironment = {\n` +
47
53
  ` production: false,\n` +
48
54
  ` version: '${version}',\n` +
@@ -54,7 +60,7 @@ const fileDefs = (sourceRoot, selected) => {
54
60
  files.push({
55
61
  name: `${sourceRoot}/environments/environment.staging.ts`,
56
62
  content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
57
- `/** Staging mirrors production. Wired via the \`staging\` configuration. */\n` +
63
+ `/** Staging mirrors production. Wired via the \`stag\` configuration. */\n` +
58
64
  `export const environment: AppEnvironment = {\n` +
59
65
  ` production: true,\n` +
60
66
  ` version: '${version}',\n` +
@@ -77,14 +83,15 @@ const fileDefs = (sourceRoot, selected) => {
77
83
  };
78
84
  /**
79
85
  * `ng add @rivinia/env` — scaffolds typed environment files and wires
80
- * development/staging/production configurations with file replacements.
86
+ * dev/stag/prod configurations with file replacements.
81
87
  *
82
88
  * - Never overwrites existing files (each file skipped individually).
83
89
  * - Never removes user configuration: only ADDS missing fileReplacements
84
- * (matched by replace+with pair) and creates a `staging` configuration
85
- * (copied from `production` when present) if absent.
86
- * - `development` intentionally gets no replacement (CLI standard: it uses
87
- * `environment.ts` directly).
90
+ * (one per source file) and creates short configurations from their legacy
91
+ * equivalents when absent.
92
+ * - `dev` intentionally gets no replacement (it uses `environment.ts`).
93
+ * Existing `development`, `staging`, and `production` configurations remain
94
+ * wired for compatibility.
88
95
  * - App version is snapshotted from the app's `package.json` (`0.0.0` fallback).
89
96
  */
90
97
  function ngAdd(options) {
@@ -127,14 +134,20 @@ function ngAdd(options) {
127
134
  created++;
128
135
  }
129
136
  if (selected.includes('prod')) {
130
- ensureReplacement(tree, angular, name, 'production', {
137
+ ensureNamedConfig(tree, angular, name, 'prod', ['production'], context);
138
+ const replacement = {
131
139
  replace: `${sourceRoot}/environments/environment.ts`,
132
140
  with: `${sourceRoot}/environments/environment.prod.ts`,
133
- });
141
+ };
142
+ ensureReplacement(tree, angular, name, 'prod', replacement);
143
+ ensureReplacement(tree, angular, name, 'production', replacement);
134
144
  }
135
145
  if (selected.includes('stage')) {
136
146
  ensureStaging(tree, angular, name, sourceRoot, context);
137
147
  }
148
+ if (selected.includes('dev')) {
149
+ ensureNamedConfig(tree, angular, name, 'dev', ['development'], context);
150
+ }
138
151
  ensureOutputPaths(tree, angular, name, selected, context);
139
152
  if (created > 0 || configsSnapshot() !== configsBefore) {
140
153
  tree.overwrite('/angular.json', JSON.stringify(angular, null, 2) + '\n');
@@ -204,18 +217,25 @@ function ensureReplacement(tree, angular, name, configuration, entry) {
204
217
  const configurations = getConfigurations(angular, name);
205
218
  const target = configurations[configuration];
206
219
  if (!target)
207
- return; // never create production/development for the user
220
+ return;
208
221
  const list = target['fileReplacements'];
209
222
  if (!Array.isArray(list)) {
210
223
  target['fileReplacements'] = [{ ...entry }];
211
224
  return;
212
225
  }
213
- const exists = list.some((item) => typeof item === 'object' &&
226
+ const matches = list
227
+ .map((item, index) => ({ item, index }))
228
+ .filter(({ item }) => typeof item === 'object' &&
214
229
  item !== null &&
215
- item['replace'] === entry.replace &&
216
- item['with'] === entry.with);
217
- if (!exists)
230
+ item['replace'] === entry.replace);
231
+ if (matches.length === 0) {
218
232
  list.push({ ...entry });
233
+ return;
234
+ }
235
+ const firstIndex = matches[0].index;
236
+ list[firstIndex] = { ...entry };
237
+ for (const { index } of matches.slice(1).reverse())
238
+ list.splice(index, 1);
219
239
  }
220
240
  /**
221
241
  * Points each selected configuration at its build directory
@@ -228,12 +248,13 @@ function ensureOutputPaths(tree, angular, name, selected, context) {
228
248
  const configurations = getConfigurations(angular, name);
229
249
  let touched = false;
230
250
  for (const env of selected) {
231
- const configName = CONFIG_NAME[env];
232
- const target = configurations[configName];
233
- if (!target || 'outputPath' in target)
234
- continue;
235
- target['outputPath'] = OUTPUT_DIR[env];
236
- touched = true;
251
+ for (const configName of [CONFIG_NAME[env], LEGACY_CONFIG_NAME[env]]) {
252
+ const target = configurations[configName];
253
+ if (!target || 'outputPath' in target)
254
+ continue;
255
+ target['outputPath'] = OUTPUT_DIR[env];
256
+ touched = true;
257
+ }
237
258
  }
238
259
  if (touched)
239
260
  ensureGitignore(tree, context);
@@ -251,17 +272,28 @@ function ensureGitignore(tree, context) {
251
272
  function ensureStaging(tree, angular, name, sourceRoot, context) {
252
273
  if (!tree.exists('/angular.json'))
253
274
  return;
254
- const configurations = getConfigurations(angular, name);
255
- if (!configurations['staging']) {
256
- const production = configurations['production'];
257
- const copy = production && typeof production === 'object' ? { ...production } : {};
258
- delete copy['fileReplacements'];
259
- delete copy['outputPath'];
260
- configurations['staging'] = copy;
261
- context.logger.info('[rivinia/env] created `staging` configuration.');
262
- }
263
- ensureReplacement(tree, angular, name, 'staging', {
275
+ ensureNamedConfig(tree, angular, name, 'stag', ['staging', 'production'], context);
276
+ ensureNamedConfig(tree, angular, name, 'staging', ['stag', 'production'], context);
277
+ const replacement = {
264
278
  replace: `${sourceRoot}/environments/environment.ts`,
265
279
  with: `${sourceRoot}/environments/environment.staging.ts`,
266
- });
280
+ };
281
+ ensureReplacement(tree, angular, name, 'stag', replacement);
282
+ ensureReplacement(tree, angular, name, 'staging', replacement);
283
+ }
284
+ function ensureNamedConfig(tree, angular, name, configuration, fallbacks, context) {
285
+ if (!tree.exists('/angular.json'))
286
+ return;
287
+ const configurations = getConfigurations(angular, name);
288
+ if (configurations[configuration])
289
+ return;
290
+ const source = fallbacks
291
+ .map((fallback) => configurations[fallback])
292
+ .find((candidate) => candidate && typeof candidate === 'object');
293
+ const copy = source
294
+ ? JSON.parse(JSON.stringify(source))
295
+ : {};
296
+ delete copy['outputPath'];
297
+ configurations[configuration] = copy;
298
+ context.logger.info(`[rivinia/env] created \`${configuration}\` configuration.`);
267
299
  }