@rivinia/env 0.1.0 → 0.3.0

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
@@ -8,23 +8,37 @@ Typed application environments for Angular: dev/staging/prod file layout, a
8
8
  files and wires the build configurations. Zero runtime code — environments are
9
9
  build-time constants; this package ships types plus setup tooling only.
10
10
 
11
- - Version: **0.1.0**
11
+ - Version: **0.3.0**
12
12
  - Requires: nothing at runtime (types only). Schematic runs under Angular CLI 22+.
13
13
 
14
14
  ## Install
15
15
 
16
16
  ```bash
17
17
  npm install @rivinia/env
18
- ng add @rivinia/env
18
+ ng add @rivinia/env # multi-select: dev / stage / prod
19
19
  ```
20
20
 
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
+
25
+ ```bash
26
+ ng add @rivinia/env --environments stage,prod
27
+ ```
28
+
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.
34
+
21
35
  `ng add` creates (never overwrites) `src/environments/`:
22
36
 
23
37
  ```
24
38
  environment.model.ts # AppEnvironment extends BaseEnvironment — extend it
25
- environment.ts # development defaults (used as-is by `development`)
26
- environment.staging.ts # staging values (production: true)
27
- environment.prod.ts # production values
39
+ environment.ts # dev defaults (used as-is by `dev`)
40
+ environment.staging.ts # stage values (production: true)
41
+ environment.prod.ts # prod values
28
42
  ```
29
43
 
30
44
  and wires `angular.json`: `production` gains its replacement if missing, and a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivinia/env",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Typed application environments for Angular (dev/staging/prod) with an ng-add schematic.",
5
5
  "keywords": [
6
6
  "angular",
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
3
3
  "schematics": {
4
4
  "ng-add": {
5
- "description": "Scaffold typed environment files and wire dev/staging/production configurations.",
5
+ "description": "Scaffold typed environment files and wire dev/stage/prod configurations.",
6
6
  "factory": "./ng-add/index.cjs#ngAdd",
7
7
  "schema": "./ng-add/schema.json"
8
8
  }
@@ -14,38 +14,67 @@ export interface AppEnvironment extends BaseEnvironment {
14
14
  readonly apiUrl: string;
15
15
  }
16
16
  `;
17
- const fileDefs = (sourceRoot) => [
18
- { name: `${sourceRoot}/environments/environment.model.ts`, content: () => MODEL_FILE },
19
- {
20
- name: `${sourceRoot}/environments/environment.ts`,
21
- content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
22
- `/** Development defaults. Used as-is by the \`development\` configuration. */\n` +
23
- `export const environment: AppEnvironment = {\n` +
24
- ` production: false,\n` +
25
- ` version: '${version}',\n` +
26
- ` apiUrl: 'http://localhost:3000/api',\n` +
27
- `};\n`,
28
- },
29
- {
30
- name: `${sourceRoot}/environments/environment.staging.ts`,
31
- content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
32
- `/** Staging mirrors production. Wired via the \`staging\` configuration. */\n` +
33
- `export const environment: AppEnvironment = {\n` +
34
- ` production: true,\n` +
35
- ` version: '${version}',\n` +
36
- ` apiUrl: 'https://staging.example.com/api',\n` +
37
- `};\n`,
38
- },
39
- {
40
- name: `${sourceRoot}/environments/environment.prod.ts`,
41
- content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
42
- `export const environment: AppEnvironment = {\n` +
43
- ` production: true,\n` +
44
- ` version: '${version}',\n` +
45
- ` apiUrl: 'https://api.example.com/api',\n` +
46
- `};\n`,
47
- },
48
- ];
17
+ const ALL_ENVIRONMENTS = ['dev', 'stage', 'prod'];
18
+ /** Short aliases users actually type (`prod`, `dev`, `stag`). */
19
+ const ENVIRONMENT_ALIASES = {
20
+ dev: 'dev',
21
+ prod: 'prod',
22
+ stag: 'stage',
23
+ stage: 'stage',
24
+ };
25
+ /** Output directory short names: builds/dev, builds/prod, builds/stag. */
26
+ const OUTPUT_DIR = {
27
+ dev: 'builds/dev',
28
+ stage: 'builds/stag',
29
+ prod: 'builds/prod',
30
+ };
31
+ /** Maps an environment name to its Angular CLI configuration name. */
32
+ const CONFIG_NAME = {
33
+ dev: 'development',
34
+ stage: 'staging',
35
+ prod: 'production',
36
+ };
37
+ const fileDefs = (sourceRoot, selected) => {
38
+ const files = [
39
+ { name: `${sourceRoot}/environments/environment.model.ts`, content: () => MODEL_FILE },
40
+ // The base file is always scaffolded: it is the `replace` source for
41
+ // prod/staging swaps (and what `development` uses directly).
42
+ {
43
+ name: `${sourceRoot}/environments/environment.ts`,
44
+ content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
45
+ `/** Development defaults. Used as-is by the \`development\` configuration. */\n` +
46
+ `export const environment: AppEnvironment = {\n` +
47
+ ` production: false,\n` +
48
+ ` version: '${version}',\n` +
49
+ ` apiUrl: 'http://localhost:3000/api',\n` +
50
+ `};\n`,
51
+ },
52
+ ];
53
+ if (selected.includes('stage')) {
54
+ files.push({
55
+ name: `${sourceRoot}/environments/environment.staging.ts`,
56
+ content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
57
+ `/** Staging mirrors production. Wired via the \`staging\` configuration. */\n` +
58
+ `export const environment: AppEnvironment = {\n` +
59
+ ` production: true,\n` +
60
+ ` version: '${version}',\n` +
61
+ ` apiUrl: 'https://staging.example.com/api',\n` +
62
+ `};\n`,
63
+ });
64
+ }
65
+ if (selected.includes('prod')) {
66
+ files.push({
67
+ name: `${sourceRoot}/environments/environment.prod.ts`,
68
+ content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
69
+ `export const environment: AppEnvironment = {\n` +
70
+ ` production: true,\n` +
71
+ ` version: '${version}',\n` +
72
+ ` apiUrl: 'https://api.example.com/api',\n` +
73
+ `};\n`,
74
+ });
75
+ }
76
+ return files;
77
+ };
49
78
  /**
50
79
  * `ng add @rivinia/env` — scaffolds typed environment files and wires
51
80
  * development/staging/production configurations with file replacements.
@@ -66,10 +95,29 @@ function ngAdd(options) {
66
95
  const { name, project, angular } = workspace;
67
96
  const sourceRoot = project.sourceRoot ?? (project.root ? `${project.root}/src` : 'src');
68
97
  const version = readAppVersion(tree);
98
+ // Empty selection (or engine default) means everything — same as --defaults.
99
+ // Accepts arrays (prompt/programmatic) and comma strings (CLI flags), with
100
+ // short aliases (dev/prod/stag/stage) normalized before validation.
101
+ const rawSelected = options.environments;
102
+ const asArray = Array.isArray(rawSelected)
103
+ ? [...rawSelected]
104
+ : typeof rawSelected === 'string'
105
+ ? rawSelected.split(',')
106
+ : [];
107
+ const cleaned = asArray
108
+ .map((e) => e.trim().toLowerCase())
109
+ .filter((e) => e.length > 0)
110
+ .map((e) => ENVIRONMENT_ALIASES[e] ?? e);
111
+ const unknown = [...new Set(cleaned)].filter((e) => !ALL_ENVIRONMENTS.includes(e));
112
+ if (unknown.length > 0) {
113
+ throw new schematics_1.SchematicsException(`[rivinia/env] unknown environment(s): ${unknown.join(', ')}. ` +
114
+ `Choose from dev, stage (stag), prod.`);
115
+ }
116
+ const selected = cleaned.length > 0 ? [...new Set(cleaned)] : [...ALL_ENVIRONMENTS];
69
117
  const configsSnapshot = () => JSON.stringify(project.architect?.build?.configurations ?? null);
70
118
  const configsBefore = configsSnapshot();
71
119
  let created = 0;
72
- for (const file of fileDefs(sourceRoot)) {
120
+ for (const file of fileDefs(sourceRoot, selected)) {
73
121
  const path = `/${file.name}`;
74
122
  if (tree.exists(path)) {
75
123
  context.logger.info(`[rivinia/env] exists, skipped: ${path}`);
@@ -78,11 +126,16 @@ function ngAdd(options) {
78
126
  tree.create(path, file.content(version));
79
127
  created++;
80
128
  }
81
- ensureReplacement(tree, angular, name, 'production', {
82
- replace: `${sourceRoot}/environments/environment.ts`,
83
- with: `${sourceRoot}/environments/environment.prod.ts`,
84
- });
85
- ensureStaging(tree, angular, name, sourceRoot, context);
129
+ if (selected.includes('prod')) {
130
+ ensureReplacement(tree, angular, name, 'production', {
131
+ replace: `${sourceRoot}/environments/environment.ts`,
132
+ with: `${sourceRoot}/environments/environment.prod.ts`,
133
+ });
134
+ }
135
+ if (selected.includes('stage')) {
136
+ ensureStaging(tree, angular, name, sourceRoot, context);
137
+ }
138
+ ensureOutputPaths(tree, angular, name, selected, context);
86
139
  if (created > 0 || configsSnapshot() !== configsBefore) {
87
140
  tree.overwrite('/angular.json', JSON.stringify(angular, null, 2) + '\n');
88
141
  }
@@ -164,6 +217,37 @@ function ensureReplacement(tree, angular, name, configuration, entry) {
164
217
  if (!exists)
165
218
  list.push({ ...entry });
166
219
  }
220
+ /**
221
+ * Points each selected configuration at its build directory
222
+ * (builds/dev, builds/stag, builds/prod) — but only when the app has not
223
+ * customized `outputPath` already. Never overwrites user output locations.
224
+ */
225
+ function ensureOutputPaths(tree, angular, name, selected, context) {
226
+ if (!tree.exists('/angular.json'))
227
+ return;
228
+ const configurations = getConfigurations(angular, name);
229
+ let touched = false;
230
+ 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;
237
+ }
238
+ if (touched)
239
+ ensureGitignore(tree, context);
240
+ }
241
+ /** Appends `/builds` to the app .gitignore once (build outputs don't commit). */
242
+ function ensureGitignore(tree, context) {
243
+ if (!tree.exists('/.gitignore'))
244
+ return;
245
+ const content = tree.read('/.gitignore')?.toString('utf-8') ?? '';
246
+ if (/(^|\n)\/*builds\/?(\n|$)/.test(content))
247
+ return;
248
+ tree.overwrite('/.gitignore', content.replace(/\s*$/, '\n') + '/builds\n');
249
+ context.logger.info('[rivinia/env] added /builds to .gitignore.');
250
+ }
167
251
  function ensureStaging(tree, angular, name, sourceRoot, context) {
168
252
  if (!tree.exists('/angular.json'))
169
253
  return;
@@ -4,9 +4,37 @@
4
4
  "title": "Rivinia env ng-add options",
5
5
  "type": "object",
6
6
  "properties": {
7
+ "environments": {
8
+ "type": ["array", "string"],
9
+ "description": "Which environments to scaffold and wire: array, or comma-separated string on the CLI (--environments stage,prod). Model + base file always. Omit for all three.",
10
+ "default": ["dev", "stage", "prod"],
11
+ "items": {
12
+ "type": "string",
13
+ "enum": ["dev", "stage", "prod", "stag"]
14
+ },
15
+ "x-prompt": {
16
+ "message": "Which environments should ng-add scaffold and wire?",
17
+ "type": "list",
18
+ "multiselect": true,
19
+ "items": [
20
+ {
21
+ "value": "dev",
22
+ "label": "dev \u2014 base file, no replacement"
23
+ },
24
+ {
25
+ "value": "stage",
26
+ "label": "stage \u2014 staging file + configuration"
27
+ },
28
+ {
29
+ "value": "prod",
30
+ "label": "prod \u2014 prod file + replacement"
31
+ }
32
+ ]
33
+ }
34
+ },
7
35
  "project": {
8
36
  "type": "string",
9
- "description": "Angular CLI project name — only for multi-app workspaces. Defaults to the workspace default project.",
37
+ "description": "Angular CLI project name \u2014 only for multi-app workspaces. Defaults to the workspace default project.",
10
38
  "default": ""
11
39
  }
12
40
  },