@rivinia/env 0.0.0-stage → 0.2.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 +68 -2
- package/fesm2022/rivinia-env.mjs +0 -0
- package/package.json +26 -3
- package/schematics/collection.json +10 -0
- package/schematics/ng-add/index.cjs +262 -0
- package/schematics/ng-add/schema.json +30 -0
- package/types/rivinia-env.d.ts +15 -0
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @rivinia/env
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@rivinia/env)
|
|
4
|
+
[](https://www.npmjs.com/package/@rivinia/env)
|
|
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.
|
|
10
|
+
|
|
11
|
+
- Version: **0.2.0**
|
|
12
|
+
- Requires: nothing at runtime (types only). Schematic runs under Angular CLI 22+.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @rivinia/env
|
|
18
|
+
ng add @rivinia/env # multi-select: development / staging / production
|
|
19
|
+
```
|
|
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 staging,production
|
|
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
|
+
|
|
35
|
+
`ng add` creates (never overwrites) `src/environments/`:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
environment.model.ts # AppEnvironment extends BaseEnvironment — extend it
|
|
39
|
+
environment.ts # development defaults (used as-is by `development`)
|
|
40
|
+
environment.staging.ts # staging values (production: true)
|
|
41
|
+
environment.prod.ts # production 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
|
+
|
|
49
|
+
## Usage pattern (app-owned)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// environment.model.ts
|
|
53
|
+
import type { BaseEnvironment } from '@rivinia/env';
|
|
54
|
+
|
|
55
|
+
export interface AppEnvironment extends BaseEnvironment {
|
|
56
|
+
readonly apiUrl: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// anywhere:
|
|
60
|
+
import { environment } from '../environments/environment';
|
|
61
|
+
fetch(`${environment.apiUrl}/trips`);
|
|
62
|
+
console.log(environment.version); // e.g. footer, about page, cache keys
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Re-run the schematic (or edit by hand) when the app version changes.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT.
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivinia/env",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Typed application environments for Angular (dev/staging/prod) with an ng-add schematic.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"angular",
|
|
7
|
+
"environment",
|
|
8
|
+
"configuration",
|
|
9
|
+
"typescript"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"schematics": "./schematics/collection.json",
|
|
14
|
+
"module": "fesm2022/rivinia-env.mjs",
|
|
15
|
+
"typings": "types/rivinia-env.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
"./package.json": {
|
|
18
|
+
"default": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./types/rivinia-env.d.ts",
|
|
22
|
+
"default": "./fesm2022/rivinia-env.mjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"tslib": "^2.3.0"
|
|
28
|
+
}
|
|
6
29
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json",
|
|
3
|
+
"schematics": {
|
|
4
|
+
"ng-add": {
|
|
5
|
+
"description": "Scaffold typed environment files and wire dev/staging/production configurations.",
|
|
6
|
+
"factory": "./ng-add/index.cjs#ngAdd",
|
|
7
|
+
"schema": "./ng-add/schema.json"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ngAdd = ngAdd;
|
|
4
|
+
const schematics_1 = require("@angular-devkit/schematics");
|
|
5
|
+
const MODEL_FILE = `import type { BaseEnvironment } from '@rivinia/env';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Application environment contract. Extend with your own fields
|
|
9
|
+
* (apiUrl, keys, feature flags, …) — nothing else in the app should
|
|
10
|
+
* import environment values except through this type.
|
|
11
|
+
*/
|
|
12
|
+
export interface AppEnvironment extends BaseEnvironment {
|
|
13
|
+
/** Base URL for API calls (per-environment value below). */
|
|
14
|
+
readonly apiUrl: string;
|
|
15
|
+
}
|
|
16
|
+
`;
|
|
17
|
+
const ALL_ENVIRONMENTS = ['development', 'staging', 'production'];
|
|
18
|
+
/** Short aliases users actually type (`prod`, `dev`, `stag`). */
|
|
19
|
+
const ENVIRONMENT_ALIASES = {
|
|
20
|
+
dev: 'development',
|
|
21
|
+
prod: 'production',
|
|
22
|
+
stag: 'staging',
|
|
23
|
+
stage: 'staging',
|
|
24
|
+
};
|
|
25
|
+
/** Output directory short names: builds/dev, builds/prod, builds/stag. */
|
|
26
|
+
const OUTPUT_DIR = {
|
|
27
|
+
development: 'builds/dev',
|
|
28
|
+
staging: 'builds/stag',
|
|
29
|
+
production: 'builds/prod',
|
|
30
|
+
};
|
|
31
|
+
const fileDefs = (sourceRoot, selected) => {
|
|
32
|
+
const files = [
|
|
33
|
+
{ name: `${sourceRoot}/environments/environment.model.ts`, content: () => MODEL_FILE },
|
|
34
|
+
// The base file is always scaffolded: it is the `replace` source for
|
|
35
|
+
// prod/staging swaps (and what `development` uses directly).
|
|
36
|
+
{
|
|
37
|
+
name: `${sourceRoot}/environments/environment.ts`,
|
|
38
|
+
content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
|
|
39
|
+
`/** Development defaults. Used as-is by the \`development\` configuration. */\n` +
|
|
40
|
+
`export const environment: AppEnvironment = {\n` +
|
|
41
|
+
` production: false,\n` +
|
|
42
|
+
` version: '${version}',\n` +
|
|
43
|
+
` apiUrl: 'http://localhost:3000/api',\n` +
|
|
44
|
+
`};\n`,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
if (selected.includes('staging')) {
|
|
48
|
+
files.push({
|
|
49
|
+
name: `${sourceRoot}/environments/environment.staging.ts`,
|
|
50
|
+
content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
|
|
51
|
+
`/** Staging mirrors production. Wired via the \`staging\` configuration. */\n` +
|
|
52
|
+
`export const environment: AppEnvironment = {\n` +
|
|
53
|
+
` production: true,\n` +
|
|
54
|
+
` version: '${version}',\n` +
|
|
55
|
+
` apiUrl: 'https://staging.example.com/api',\n` +
|
|
56
|
+
`};\n`,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (selected.includes('production')) {
|
|
60
|
+
files.push({
|
|
61
|
+
name: `${sourceRoot}/environments/environment.prod.ts`,
|
|
62
|
+
content: (version) => `import type { AppEnvironment } from './environment.model';\n\n` +
|
|
63
|
+
`export const environment: AppEnvironment = {\n` +
|
|
64
|
+
` production: true,\n` +
|
|
65
|
+
` version: '${version}',\n` +
|
|
66
|
+
` apiUrl: 'https://api.example.com/api',\n` +
|
|
67
|
+
`};\n`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return files;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* `ng add @rivinia/env` — scaffolds typed environment files and wires
|
|
74
|
+
* development/staging/production configurations with file replacements.
|
|
75
|
+
*
|
|
76
|
+
* - Never overwrites existing files (each file skipped individually).
|
|
77
|
+
* - Never removes user configuration: only ADDS missing fileReplacements
|
|
78
|
+
* (matched by replace+with pair) and creates a `staging` configuration
|
|
79
|
+
* (copied from `production` when present) if absent.
|
|
80
|
+
* - `development` intentionally gets no replacement (CLI standard: it uses
|
|
81
|
+
* `environment.ts` directly).
|
|
82
|
+
* - App version is snapshotted from the app's `package.json` (`0.0.0` fallback).
|
|
83
|
+
*/
|
|
84
|
+
function ngAdd(options) {
|
|
85
|
+
return (tree, context) => {
|
|
86
|
+
const workspace = readWorkspace(tree, context, options.project);
|
|
87
|
+
if (!workspace)
|
|
88
|
+
return tree;
|
|
89
|
+
const { name, project, angular } = workspace;
|
|
90
|
+
const sourceRoot = project.sourceRoot ?? (project.root ? `${project.root}/src` : 'src');
|
|
91
|
+
const version = readAppVersion(tree);
|
|
92
|
+
// Empty selection (or engine default) means everything — same as --defaults.
|
|
93
|
+
// Accepts arrays (prompt/programmatic) and comma strings (CLI flags), with
|
|
94
|
+
// short aliases (dev/prod/stag/stage) normalized before validation.
|
|
95
|
+
const rawSelected = options.environments;
|
|
96
|
+
const asArray = Array.isArray(rawSelected)
|
|
97
|
+
? [...rawSelected]
|
|
98
|
+
: typeof rawSelected === 'string'
|
|
99
|
+
? rawSelected.split(',')
|
|
100
|
+
: [];
|
|
101
|
+
const cleaned = asArray
|
|
102
|
+
.map((e) => e.trim().toLowerCase())
|
|
103
|
+
.filter((e) => e.length > 0)
|
|
104
|
+
.map((e) => ENVIRONMENT_ALIASES[e] ?? e);
|
|
105
|
+
const unknown = [...new Set(cleaned)].filter((e) => !ALL_ENVIRONMENTS.includes(e));
|
|
106
|
+
if (unknown.length > 0) {
|
|
107
|
+
throw new schematics_1.SchematicsException(`[rivinia/env] unknown environment(s): ${unknown.join(', ')}. ` +
|
|
108
|
+
`Choose from development (dev), staging (stag), production (prod).`);
|
|
109
|
+
}
|
|
110
|
+
const selected = cleaned.length > 0
|
|
111
|
+
? [...new Set(cleaned)]
|
|
112
|
+
: [...ALL_ENVIRONMENTS];
|
|
113
|
+
const configsSnapshot = () => JSON.stringify(project.architect?.build?.configurations ?? null);
|
|
114
|
+
const configsBefore = configsSnapshot();
|
|
115
|
+
let created = 0;
|
|
116
|
+
for (const file of fileDefs(sourceRoot, selected)) {
|
|
117
|
+
const path = `/${file.name}`;
|
|
118
|
+
if (tree.exists(path)) {
|
|
119
|
+
context.logger.info(`[rivinia/env] exists, skipped: ${path}`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
tree.create(path, file.content(version));
|
|
123
|
+
created++;
|
|
124
|
+
}
|
|
125
|
+
if (selected.includes('production')) {
|
|
126
|
+
ensureReplacement(tree, angular, name, 'production', {
|
|
127
|
+
replace: `${sourceRoot}/environments/environment.ts`,
|
|
128
|
+
with: `${sourceRoot}/environments/environment.prod.ts`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (selected.includes('staging')) {
|
|
132
|
+
ensureStaging(tree, angular, name, sourceRoot, context);
|
|
133
|
+
}
|
|
134
|
+
ensureOutputPaths(tree, angular, name, selected, context);
|
|
135
|
+
if (created > 0 || configsSnapshot() !== configsBefore) {
|
|
136
|
+
tree.overwrite('/angular.json', JSON.stringify(angular, null, 2) + '\n');
|
|
137
|
+
}
|
|
138
|
+
context.logger.info(`[rivinia/env] ${created} file(s) created, configurations wired for '${name}'.`);
|
|
139
|
+
return tree;
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function readWorkspace(tree, context, requested) {
|
|
143
|
+
let angular;
|
|
144
|
+
try {
|
|
145
|
+
const raw = tree.read('/angular.json')?.toString('utf-8');
|
|
146
|
+
if (!raw)
|
|
147
|
+
throw new schematics_1.SchematicsException('angular.json not found.');
|
|
148
|
+
angular = JSON.parse(raw);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
context.logger.warn(`[rivinia/env] cannot read angular.json (${error instanceof Error ? error.message : error}) — skipping.`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const names = Object.keys(angular.projects ?? {});
|
|
155
|
+
if (names.length === 0) {
|
|
156
|
+
context.logger.warn('[rivinia/env] no projects in angular.json — skipping.');
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
// NOTE: the schema default for `project` is "" (engine-filled), so only a
|
|
160
|
+
// non-empty value counts as an explicit request. (`??` alone would honor "".)
|
|
161
|
+
const name = requested && requested.length > 0
|
|
162
|
+
? requested
|
|
163
|
+
: angular.defaultProject && typeof angular.defaultProject === 'string'
|
|
164
|
+
? angular.defaultProject
|
|
165
|
+
: names[0];
|
|
166
|
+
const project = angular.projects?.[name];
|
|
167
|
+
if (!project) {
|
|
168
|
+
context.logger.warn(`[rivinia/env] project '${name}' not found — skipping.`);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return { name, project, angular };
|
|
172
|
+
}
|
|
173
|
+
function readAppVersion(tree) {
|
|
174
|
+
try {
|
|
175
|
+
const raw = tree.read('/package.json')?.toString('utf-8');
|
|
176
|
+
if (!raw)
|
|
177
|
+
return '0.0.0';
|
|
178
|
+
const parsed = JSON.parse(raw);
|
|
179
|
+
return typeof parsed.version === 'string' && parsed.version.length > 0
|
|
180
|
+
? parsed.version
|
|
181
|
+
: '0.0.0';
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return '0.0.0';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function getConfigurations(angular, name) {
|
|
188
|
+
const project = angular.projects?.[name];
|
|
189
|
+
if (!project)
|
|
190
|
+
throw new schematics_1.SchematicsException(`[rivinia/env] project '${name}' not found.`);
|
|
191
|
+
project.architect ??= {};
|
|
192
|
+
project.architect.build ??= {};
|
|
193
|
+
const build = project.architect.build;
|
|
194
|
+
build.configurations ??= {};
|
|
195
|
+
return build.configurations;
|
|
196
|
+
}
|
|
197
|
+
function ensureReplacement(tree, angular, name, configuration, entry) {
|
|
198
|
+
if (!tree.exists('/angular.json'))
|
|
199
|
+
return;
|
|
200
|
+
const configurations = getConfigurations(angular, name);
|
|
201
|
+
const target = configurations[configuration];
|
|
202
|
+
if (!target)
|
|
203
|
+
return; // never create production/development for the user
|
|
204
|
+
const list = target['fileReplacements'];
|
|
205
|
+
if (!Array.isArray(list)) {
|
|
206
|
+
target['fileReplacements'] = [{ ...entry }];
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const exists = list.some((item) => typeof item === 'object' &&
|
|
210
|
+
item !== null &&
|
|
211
|
+
item['replace'] === entry.replace &&
|
|
212
|
+
item['with'] === entry.with);
|
|
213
|
+
if (!exists)
|
|
214
|
+
list.push({ ...entry });
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Points each selected configuration at its build directory
|
|
218
|
+
* (builds/dev, builds/stag, builds/prod) — but only when the app has not
|
|
219
|
+
* customized `outputPath` already. Never overwrites user output locations.
|
|
220
|
+
*/
|
|
221
|
+
function ensureOutputPaths(tree, angular, name, selected, context) {
|
|
222
|
+
if (!tree.exists('/angular.json'))
|
|
223
|
+
return;
|
|
224
|
+
const configurations = getConfigurations(angular, name);
|
|
225
|
+
let touched = false;
|
|
226
|
+
for (const env of selected) {
|
|
227
|
+
const target = configurations[env];
|
|
228
|
+
if (!target || 'outputPath' in target)
|
|
229
|
+
continue;
|
|
230
|
+
target['outputPath'] = OUTPUT_DIR[env];
|
|
231
|
+
touched = true;
|
|
232
|
+
}
|
|
233
|
+
if (touched)
|
|
234
|
+
ensureGitignore(tree, context);
|
|
235
|
+
}
|
|
236
|
+
/** Appends `/builds` to the app .gitignore once (build outputs don't commit). */
|
|
237
|
+
function ensureGitignore(tree, context) {
|
|
238
|
+
if (!tree.exists('/.gitignore'))
|
|
239
|
+
return;
|
|
240
|
+
const content = tree.read('/.gitignore')?.toString('utf-8') ?? '';
|
|
241
|
+
if (/(^|\n)\/*builds\/?(\n|$)/.test(content))
|
|
242
|
+
return;
|
|
243
|
+
tree.overwrite('/.gitignore', content.replace(/\s*$/, '\n') + '/builds\n');
|
|
244
|
+
context.logger.info('[rivinia/env] added /builds to .gitignore.');
|
|
245
|
+
}
|
|
246
|
+
function ensureStaging(tree, angular, name, sourceRoot, context) {
|
|
247
|
+
if (!tree.exists('/angular.json'))
|
|
248
|
+
return;
|
|
249
|
+
const configurations = getConfigurations(angular, name);
|
|
250
|
+
if (!configurations['staging']) {
|
|
251
|
+
const production = configurations['production'];
|
|
252
|
+
const copy = production && typeof production === 'object' ? { ...production } : {};
|
|
253
|
+
delete copy['fileReplacements'];
|
|
254
|
+
delete copy['outputPath'];
|
|
255
|
+
configurations['staging'] = copy;
|
|
256
|
+
context.logger.info('[rivinia/env] created `staging` configuration.');
|
|
257
|
+
}
|
|
258
|
+
ensureReplacement(tree, angular, name, 'staging', {
|
|
259
|
+
replace: `${sourceRoot}/environments/environment.ts`,
|
|
260
|
+
with: `${sourceRoot}/environments/environment.staging.ts`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "rivinia-env-ng-add",
|
|
4
|
+
"title": "Rivinia env ng-add options",
|
|
5
|
+
"type": "object",
|
|
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 staging,production). Model + base file always. Omit for all three.",
|
|
10
|
+
"default": ["development", "staging", "production"],
|
|
11
|
+
"items": { "type": "string", "enum": ["development", "staging", "production", "dev", "prod", "stag", "stage"] },
|
|
12
|
+
"x-prompt": {
|
|
13
|
+
"message": "Which environments should ng-add scaffold and wire?",
|
|
14
|
+
"type": "list",
|
|
15
|
+
"multiselect": true,
|
|
16
|
+
"items": [
|
|
17
|
+
{ "value": "development", "label": "development — base file, no replacement" },
|
|
18
|
+
{ "value": "staging", "label": "staging — staging file + configuration" },
|
|
19
|
+
{ "value": "production", "label": "production — prod file + replacement" }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"project": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"description": "Angular CLI project name — only for multi-app workspaces. Defaults to the workspace default project.",
|
|
26
|
+
"default": ""
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"required": []
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal contract every Rivinia-style environment file satisfies.
|
|
3
|
+
* Applications extend it with their own fields (apiUrl, keys, …) — the
|
|
4
|
+
* library never dictates field names beyond these two.
|
|
5
|
+
*/
|
|
6
|
+
interface BaseEnvironment {
|
|
7
|
+
/** `true` for production-like builds (staging included). */
|
|
8
|
+
readonly production: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Application version, snapshotted from the app's `package.json` when the
|
|
11
|
+
* files are scaffolded. Re-run the schematic or edit by hand on release.
|
|
12
|
+
*/
|
|
13
|
+
readonly version: string;
|
|
14
|
+
}
|
|
15
|
+
export type { BaseEnvironment };
|