react-native-config-ultimate 0.0.5 → 0.0.7
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/package.json +12 -4
- package/index.ts +0 -37
- package/src/cli.ts +0 -158
- package/src/flatten.ts +0 -26
- package/src/load-env.ts +0 -80
- package/src/main.ts +0 -39
- package/src/render-env.ts +0 -111
- package/src/resolve-env.ts +0 -45
- package/src/validate-env.ts +0 -71
- package/src/write-env.ts +0 -74
package/package.json
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-config-ultimate",
|
|
3
3
|
"title": "React Native Config Ultimate",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"description": "Config that works. A community-maintained fork of react-native-ultimate-config.",
|
|
6
6
|
"main": "index.js",
|
|
7
|
+
"module": "index.js",
|
|
7
8
|
"react-native": "index.js",
|
|
9
|
+
"source": "index.ts",
|
|
8
10
|
"browser": "index.web.js",
|
|
9
11
|
"types": "index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"react-native": "./index.js",
|
|
15
|
+
"import": "./index.js",
|
|
16
|
+
"require": "./index.js",
|
|
17
|
+
"default": "./index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
10
20
|
"files": [
|
|
11
21
|
"bin.js",
|
|
12
22
|
"index.js",
|
|
@@ -14,10 +24,8 @@
|
|
|
14
24
|
"index.web.js",
|
|
15
25
|
"src/*.js",
|
|
16
26
|
"!src/*.spec.js",
|
|
17
|
-
"src
|
|
18
|
-
"!src/*.spec.ts",
|
|
27
|
+
"src/NativeUltimateConfig.ts",
|
|
19
28
|
"src/templates",
|
|
20
|
-
"index.ts",
|
|
21
29
|
"override.js",
|
|
22
30
|
"ios/UltimateConfig.{h,mm}",
|
|
23
31
|
"ios/ConfigValues.h",
|
package/index.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { TurboModuleRegistry, NativeModules } from 'react-native';
|
|
2
|
-
import type { Spec } from './src/NativeUltimateConfig';
|
|
3
|
-
|
|
4
|
-
export type ConfigValue = string | number | boolean;
|
|
5
|
-
export type { Spec } from './src/NativeUltimateConfig';
|
|
6
|
-
|
|
7
|
-
type Config = Record<string, ConfigValue>;
|
|
8
|
-
|
|
9
|
-
// override.js is dynamically generated by the rncu CLI.
|
|
10
|
-
// It contains platform-specific overrides. Do not commit override.js to git.
|
|
11
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
12
|
-
const override: Config = require('./override');
|
|
13
|
-
|
|
14
|
-
// New Architecture: TurboModules are accessed via TurboModuleRegistry.
|
|
15
|
-
// The spec exposes getAll(): string — returns config values as JSON.
|
|
16
|
-
// Old Architecture: constants are exposed directly on NativeModules via constantsToExport.
|
|
17
|
-
const turboModule = TurboModuleRegistry.get<Spec>('UltimateConfig');
|
|
18
|
-
|
|
19
|
-
let nativeConstants: Config;
|
|
20
|
-
|
|
21
|
-
if (turboModule != null && typeof turboModule.getAll === 'function') {
|
|
22
|
-
// New Arch: call getAll() and parse JSON
|
|
23
|
-
try {
|
|
24
|
-
const raw = turboModule.getAll();
|
|
25
|
-
nativeConstants = JSON.parse(raw) as Config;
|
|
26
|
-
} catch {
|
|
27
|
-
nativeConstants = {};
|
|
28
|
-
}
|
|
29
|
-
} else {
|
|
30
|
-
// Old Arch / interop bridge: constants are properties on NativeModules.UltimateConfig
|
|
31
|
-
nativeConstants = (NativeModules.UltimateConfig as Config | undefined) ?? {};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export default {
|
|
35
|
-
...nativeConstants,
|
|
36
|
-
...override,
|
|
37
|
-
} as Config;
|
package/src/cli.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import yargs from 'yargs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import { watch } from 'chokidar';
|
|
5
|
-
import main from './main';
|
|
6
|
-
import type { RC } from './resolve-env';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Load the RC file fresh on every call (clears require cache so
|
|
10
|
-
* changes to .rncurc.js are picked up during --watch mode).
|
|
11
|
-
*/
|
|
12
|
-
function load_rc(rc_file: string): RC | undefined {
|
|
13
|
-
if (!fs.existsSync(rc_file)) return undefined;
|
|
14
|
-
delete require.cache[require.resolve(rc_file)];
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
|
|
16
|
-
return require(rc_file) as RC;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function log(msg: string): void {
|
|
20
|
-
process.stdout.write(`[rncu] ${msg}\n`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function log_err(msg: string): void {
|
|
24
|
-
process.stderr.write(`[rncu] ✗ ${msg}\n`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export default async function cli(): Promise<void> {
|
|
28
|
-
const argv = await yargs(process.argv.slice(2))
|
|
29
|
-
.option('projectRoot', {
|
|
30
|
-
type: 'string',
|
|
31
|
-
default: process.cwd(),
|
|
32
|
-
description: 'Root directory of the React Native project',
|
|
33
|
-
})
|
|
34
|
-
.option('libRoot', {
|
|
35
|
-
type: 'string',
|
|
36
|
-
description:
|
|
37
|
-
'Root directory of react-native-config-ultimate ' +
|
|
38
|
-
'(defaults to <projectRoot>/node_modules/react-native-config-ultimate)',
|
|
39
|
-
})
|
|
40
|
-
.option('watch', {
|
|
41
|
-
alias: 'w',
|
|
42
|
-
type: 'boolean',
|
|
43
|
-
default: false,
|
|
44
|
-
description:
|
|
45
|
-
'Watch env file(s) for changes and regenerate automatically. ' +
|
|
46
|
-
'Note: changes to native vars (iOS xcconfig, Android BuildConfig) ' +
|
|
47
|
-
'still require a full native rebuild.',
|
|
48
|
-
})
|
|
49
|
-
.usage('Usage: $0 <env-file> [env-file2 ...] [options]')
|
|
50
|
-
.help()
|
|
51
|
-
.parseAsync();
|
|
52
|
-
|
|
53
|
-
const project_root = argv.projectRoot;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Resolve the library root directory.
|
|
57
|
-
*
|
|
58
|
-
* Priority:
|
|
59
|
-
* 1. --libRoot flag (explicit override)
|
|
60
|
-
* 2. Conventional path <projectRoot>/node_modules/react-native-config-ultimate
|
|
61
|
-
* — used when the directory exists (standard install, or bin.spec.ts integration test).
|
|
62
|
-
* 3. require.resolve() — handles npm workspaces hoisting, pnpm, Yarn PnP,
|
|
63
|
-
* and any layout where the package is hoisted above projectRoot.
|
|
64
|
-
* 4. Fall back to conventional path even if it doesn't exist yet
|
|
65
|
-
* (write-env.ts will create the directories on first run).
|
|
66
|
-
*/
|
|
67
|
-
const lib_root: string = (() => {
|
|
68
|
-
if (argv.libRoot) return argv.libRoot;
|
|
69
|
-
|
|
70
|
-
const conventional = path.join(project_root, 'node_modules', 'react-native-config-ultimate');
|
|
71
|
-
|
|
72
|
-
// If the directory already exists at the conventional location, use it.
|
|
73
|
-
// This handles standard installs and the integration test temp-dir setup.
|
|
74
|
-
if (fs.existsSync(conventional)) return conventional;
|
|
75
|
-
|
|
76
|
-
// Otherwise, try require.resolve to handle hoisted workspaces.
|
|
77
|
-
try {
|
|
78
|
-
const pkg_json = require.resolve('react-native-config-ultimate/package.json', {
|
|
79
|
-
paths: [project_root],
|
|
80
|
-
});
|
|
81
|
-
return path.dirname(pkg_json);
|
|
82
|
-
} catch {
|
|
83
|
-
// Last resort: return the conventional path and let write-env create it.
|
|
84
|
-
return conventional;
|
|
85
|
-
}
|
|
86
|
-
})();
|
|
87
|
-
|
|
88
|
-
// Accept one or more positional env file paths.
|
|
89
|
-
// Multiple files are merged left-to-right (last file wins for conflicting keys).
|
|
90
|
-
const env_files = argv._.map(String);
|
|
91
|
-
|
|
92
|
-
// Validate env files exist before running anything.
|
|
93
|
-
const missing_files = env_files.filter((f) => !fs.existsSync(f));
|
|
94
|
-
if (missing_files.length > 0) {
|
|
95
|
-
for (const f of missing_files) {
|
|
96
|
-
log_err(`env file not found: ${f}`);
|
|
97
|
-
}
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const rc_file = path.resolve(project_root, '.rncurc.js');
|
|
102
|
-
|
|
103
|
-
// Helper: run the full pipeline once, returning duration in ms.
|
|
104
|
-
// Never throws — errors are caught and logged so watch mode stays alive.
|
|
105
|
-
async function run(changed_path?: string): Promise<void> {
|
|
106
|
-
if (changed_path) {
|
|
107
|
-
log(`${changed_path} changed → regenerating...`);
|
|
108
|
-
}
|
|
109
|
-
const start = Date.now();
|
|
110
|
-
try {
|
|
111
|
-
const rc = load_rc(rc_file);
|
|
112
|
-
await main(project_root, lib_root, env_files, rc);
|
|
113
|
-
if (changed_path) {
|
|
114
|
-
log(`✓ done in ${Date.now() - start}ms`);
|
|
115
|
-
}
|
|
116
|
-
} catch (err) {
|
|
117
|
-
log_err(err instanceof Error ? err.message : String(err));
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Initial run (always runs, throws on error in non-watch mode).
|
|
122
|
-
if (!argv.watch) {
|
|
123
|
-
const rc = load_rc(rc_file);
|
|
124
|
-
await main(project_root, lib_root, env_files, rc);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// --watch mode ────────────────────────────────────────────────────────────
|
|
129
|
-
// Initial run (errors are caught — we still want to start watching).
|
|
130
|
-
await run();
|
|
131
|
-
|
|
132
|
-
// Files to watch: env files + RC file (if it exists).
|
|
133
|
-
const files_to_watch = [...env_files, ...(fs.existsSync(rc_file) ? [rc_file] : [])];
|
|
134
|
-
|
|
135
|
-
const watcher = watch(files_to_watch, {
|
|
136
|
-
ignoreInitial: true,
|
|
137
|
-
persistent: true,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
log(
|
|
141
|
-
`watching: ${files_to_watch.join(', ')}\n` +
|
|
142
|
-
`[rncu] ⚠ native vars (xcconfig/BuildConfig) require a full rebuild to take effect`
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
watcher.on('change', (p) => void run(p));
|
|
146
|
-
watcher.on('add', (p) => void run(p));
|
|
147
|
-
|
|
148
|
-
// Keep the process alive (chokidar persistent:true already does this,
|
|
149
|
-
// but stdin.resume makes it explicit and survives edge cases).
|
|
150
|
-
process.stdin.resume();
|
|
151
|
-
|
|
152
|
-
// Graceful shutdown on Ctrl+C.
|
|
153
|
-
process.on('SIGINT', () => {
|
|
154
|
-
process.stdout.write('\n');
|
|
155
|
-
log('stopping...');
|
|
156
|
-
void watcher.close().then(() => process.exit(0));
|
|
157
|
-
});
|
|
158
|
-
}
|
package/src/flatten.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import assert from 'assert';
|
|
2
|
-
|
|
3
|
-
type Platform = 'ios' | 'android' | 'web';
|
|
4
|
-
export type ConfigValue = string | number | boolean;
|
|
5
|
-
export type PerPlatformValue = Partial<Record<Platform, ConfigValue>>;
|
|
6
|
-
export type EnvConfig = Record<string, ConfigValue | PerPlatformValue>;
|
|
7
|
-
export type FlatConfig = Record<string, ConfigValue | undefined>;
|
|
8
|
-
|
|
9
|
-
const VALID_PLATFORMS: Platform[] = ['ios', 'android', 'web'];
|
|
10
|
-
|
|
11
|
-
export default function flatten(config: EnvConfig, platform: Platform): FlatConfig {
|
|
12
|
-
assert(config && typeof config === 'object', 'Config should be non-null object');
|
|
13
|
-
assert(
|
|
14
|
-
VALID_PLATFORMS.includes(platform),
|
|
15
|
-
'`platform` should one of: ' + VALID_PLATFORMS.join(', ')
|
|
16
|
-
);
|
|
17
|
-
const result: FlatConfig = {};
|
|
18
|
-
for (const [key, value] of Object.entries(config)) {
|
|
19
|
-
if (value && typeof value === 'object') {
|
|
20
|
-
result[key] = (value as PerPlatformValue)[platform];
|
|
21
|
-
} else {
|
|
22
|
-
result[key] = value as ConfigValue;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return result;
|
|
26
|
-
}
|
package/src/load-env.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import * as dotenv from 'dotenv';
|
|
2
|
-
import { expand } from 'dotenv-expand';
|
|
3
|
-
import * as yaml from 'js-yaml';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
|
|
7
|
-
import type { EnvData } from './resolve-env';
|
|
8
|
-
|
|
9
|
-
type FileFormat = 'dotenv' | 'yaml';
|
|
10
|
-
|
|
11
|
-
function detect_format(config_path: string): FileFormat {
|
|
12
|
-
const { ext } = path.parse(config_path);
|
|
13
|
-
return ext === '.yml' || ext === '.yaml' ? 'yaml' : 'dotenv';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function read_yaml(config_path: string): EnvData {
|
|
17
|
-
const data = yaml.load(fs.readFileSync(config_path).toString());
|
|
18
|
-
if (typeof data === 'undefined' || data === null || typeof data !== 'object') {
|
|
19
|
-
throw new Error(`Expected to read object from ${config_path}, but got '${data}'`);
|
|
20
|
-
}
|
|
21
|
-
return data as EnvData;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Load one or more env files and merge them (last file wins for conflicting keys).
|
|
26
|
-
*
|
|
27
|
-
* Dotenv files (.env, .env.staging, etc.):
|
|
28
|
-
* - All files are merged first, then variable expansion runs once.
|
|
29
|
-
* - This means cross-file `$VAR` references work:
|
|
30
|
-
* .env.base: BASE_URL=https://api.example.com
|
|
31
|
-
* .env.staging: API_URL=$BASE_URL/v1 → https://api.example.com/v1
|
|
32
|
-
*
|
|
33
|
-
* YAML files (.yml, .yaml):
|
|
34
|
-
* - Each file is loaded and shallow-merged (last wins for top-level keys).
|
|
35
|
-
* - No variable expansion is applied (use YAML anchors instead).
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* // Single file (backward-compatible):
|
|
39
|
-
* load_env('.env')
|
|
40
|
-
*
|
|
41
|
-
* // Multi-file merge:
|
|
42
|
-
* load_env(['.env.base', '.env.staging'])
|
|
43
|
-
*/
|
|
44
|
-
export default function load_env(config_paths: string | string[]): EnvData {
|
|
45
|
-
const paths = Array.isArray(config_paths) ? config_paths : [config_paths];
|
|
46
|
-
|
|
47
|
-
if (paths.length === 0) {
|
|
48
|
-
throw new Error('No env file specified. Usage: rncu <env-file> [env-file2 ...]');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const formats = paths.map(detect_format);
|
|
52
|
-
const allDotenv = formats.every((f) => f === 'dotenv');
|
|
53
|
-
|
|
54
|
-
if (allDotenv) {
|
|
55
|
-
// Merge raw parsed content first, then expand once —
|
|
56
|
-
// so cross-file $VAR references resolve correctly.
|
|
57
|
-
const raw: Record<string, string> = {};
|
|
58
|
-
for (const p of paths) {
|
|
59
|
-
const content = fs.readFileSync(p, 'utf8');
|
|
60
|
-
Object.assign(raw, dotenv.parse(content));
|
|
61
|
-
}
|
|
62
|
-
const result = expand({ parsed: raw });
|
|
63
|
-
return (result.parsed ?? raw) as EnvData;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// YAML or mixed: load each file individually and shallow-merge.
|
|
67
|
-
const merged: EnvData = {};
|
|
68
|
-
for (let i = 0; i < paths.length; i++) {
|
|
69
|
-
const p = paths[i];
|
|
70
|
-
if (formats[i] === 'yaml') {
|
|
71
|
-
Object.assign(merged, read_yaml(p));
|
|
72
|
-
} else {
|
|
73
|
-
const content = fs.readFileSync(p, 'utf8');
|
|
74
|
-
const parsed = dotenv.parse(content);
|
|
75
|
-
const expanded = expand({ parsed });
|
|
76
|
-
Object.assign(merged, expanded.parsed ?? parsed);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return merged;
|
|
80
|
-
}
|
package/src/main.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import load_env from './load-env';
|
|
2
|
-
import render_env from './render-env';
|
|
3
|
-
import write_env from './write-env';
|
|
4
|
-
import flatten from './flatten';
|
|
5
|
-
import resolve_env from './resolve-env';
|
|
6
|
-
import { validate_env } from './validate-env';
|
|
7
|
-
|
|
8
|
-
import type { RC, EnvData } from './resolve-env';
|
|
9
|
-
import type { EnvConfig } from './flatten';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Main build-time pipeline:
|
|
13
|
-
* load → resolve (on_env hook) → validate (schema) → flatten → render → write
|
|
14
|
-
*
|
|
15
|
-
* @param project_root Root of the React Native project
|
|
16
|
-
* @param lib_root Root of the react-native-config-ultimate install
|
|
17
|
-
* @param env_file Path(s) to env file(s). Multiple files are merged (last wins).
|
|
18
|
-
* @param rc Optional RC config from `.rncurc.js`
|
|
19
|
-
*/
|
|
20
|
-
export default async function main(
|
|
21
|
-
project_root: string,
|
|
22
|
-
lib_root: string,
|
|
23
|
-
env_file: string | string[],
|
|
24
|
-
rc?: RC
|
|
25
|
-
): Promise<void> {
|
|
26
|
-
const env: EnvData = await resolve_env(load_env(env_file), rc);
|
|
27
|
-
|
|
28
|
-
if (rc?.schema) {
|
|
29
|
-
validate_env(env, rc.schema);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const flat = {
|
|
33
|
-
ios: flatten(env as EnvConfig, 'ios'),
|
|
34
|
-
android: flatten(env as EnvConfig, 'android'),
|
|
35
|
-
web: flatten(env as EnvConfig, 'web'),
|
|
36
|
-
};
|
|
37
|
-
const files_to_write = render_env(project_root, lib_root, flat, rc);
|
|
38
|
-
write_env(files_to_write);
|
|
39
|
-
}
|
package/src/render-env.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import handlebars from 'handlebars';
|
|
4
|
-
|
|
5
|
-
import type { FlatConfig } from './flatten';
|
|
6
|
-
import type { RC } from './resolve-env';
|
|
7
|
-
import type { FileMap } from './write-env';
|
|
8
|
-
|
|
9
|
-
const code_file_name = 'ConfigValues';
|
|
10
|
-
const config_file_name = 'rncu';
|
|
11
|
-
|
|
12
|
-
// ─── Handlebars helpers (registered once at module load, not per-call) ────────
|
|
13
|
-
|
|
14
|
-
function is_string(value: unknown): boolean {
|
|
15
|
-
return typeof value === 'string';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function is_number(value: unknown): boolean {
|
|
19
|
-
return typeof value === 'number';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function is_boolean(value: unknown): boolean {
|
|
23
|
-
return typeof value === 'boolean';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function escape(value: unknown): unknown {
|
|
27
|
-
if (is_string(value)) {
|
|
28
|
-
return (value as string)
|
|
29
|
-
.replace(/\\/gm, '\\\\')
|
|
30
|
-
.replace(/"/gm, '\\"')
|
|
31
|
-
.replace(/\n/gm, '\\n')
|
|
32
|
-
.replace(/\r/gm, '\\r');
|
|
33
|
-
}
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function xcconfig_format(value: unknown): unknown {
|
|
38
|
-
if (is_string(value)) {
|
|
39
|
-
return (value as string).replace(/\/\//gm, '/$()/');
|
|
40
|
-
}
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function to_json(value: unknown): string {
|
|
45
|
-
return JSON.stringify(value, null, 2);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
handlebars.registerHelper('isBoolean', is_boolean);
|
|
49
|
-
handlebars.registerHelper('isString', is_string);
|
|
50
|
-
handlebars.registerHelper('isNumber', is_number);
|
|
51
|
-
handlebars.registerHelper('escape', escape);
|
|
52
|
-
handlebars.registerHelper('xcconfigFormat', xcconfig_format);
|
|
53
|
-
handlebars.registerHelper('toJSON', to_json);
|
|
54
|
-
|
|
55
|
-
// ─── Template cache (compiled once per template name, not per build) ──────────
|
|
56
|
-
|
|
57
|
-
const template_cache = new Map<string, HandlebarsTemplateDelegate>();
|
|
58
|
-
|
|
59
|
-
function get_compiled_template(template_name: string): HandlebarsTemplateDelegate {
|
|
60
|
-
const cached = template_cache.get(template_name);
|
|
61
|
-
if (cached) return cached;
|
|
62
|
-
|
|
63
|
-
const template_path = path.join(__dirname, 'templates', `${template_name}.handlebars`);
|
|
64
|
-
const compiled = handlebars.compile(fs.readFileSync(template_path, 'utf8'));
|
|
65
|
-
template_cache.set(template_name, compiled);
|
|
66
|
-
return compiled;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function render_template(template_name: string, data: unknown): string {
|
|
70
|
-
return get_compiled_template(template_name)(data);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
interface PlatformEnv {
|
|
76
|
-
ios: FlatConfig;
|
|
77
|
-
android: FlatConfig;
|
|
78
|
-
web: FlatConfig;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export default function render_env(
|
|
82
|
-
project_root: string,
|
|
83
|
-
lib_root: string,
|
|
84
|
-
env: PlatformEnv,
|
|
85
|
-
rc?: RC
|
|
86
|
-
): FileMap {
|
|
87
|
-
const { ios, android, web } = env;
|
|
88
|
-
const map: FileMap = {
|
|
89
|
-
[path.join(lib_root, 'index.d.ts')]: render_template('index.d.ts', ios),
|
|
90
|
-
[path.join(lib_root, 'index.web.js')]: render_template('index.web.js', web),
|
|
91
|
-
[path.join(lib_root, 'ios', `${code_file_name}.h`)]: render_template('ConfigValues.h', ios),
|
|
92
|
-
[path.join(lib_root, 'android', 'rncu.yaml')]: render_template('rncu.yaml', android),
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// Only write xcconfig if the project has an ios folder.
|
|
96
|
-
// All RN apps have it; some react-native-web apps may not.
|
|
97
|
-
if (fs.existsSync(path.join(project_root, 'ios'))) {
|
|
98
|
-
map[path.join(project_root, 'ios', `${config_file_name}.xcconfig`)] = render_template(
|
|
99
|
-
'rncu.xcconfig',
|
|
100
|
-
ios
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const js_override = rc && typeof rc.js_override === 'boolean' && rc.js_override;
|
|
105
|
-
map[path.join(lib_root, 'override.js')] = render_template('override.js', {
|
|
106
|
-
ios: js_override ? ios : {},
|
|
107
|
-
android: js_override ? android : {},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
return map;
|
|
111
|
-
}
|
package/src/resolve-env.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
export type EnvData = Record<string, unknown>;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Schema definition for a single environment variable.
|
|
5
|
-
* Used in `.rncurc.js` to validate env vars at build time.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* module.exports = {
|
|
9
|
-
* schema: {
|
|
10
|
-
* API_URL: { type: 'string', required: true },
|
|
11
|
-
* TIMEOUT_MS: { type: 'number', required: true },
|
|
12
|
-
* DEBUG: { type: 'boolean', required: false },
|
|
13
|
-
* ENV_NAME: { type: 'string', required: true, pattern: '^(dev|staging|prod)$' },
|
|
14
|
-
* }
|
|
15
|
-
* };
|
|
16
|
-
*/
|
|
17
|
-
export interface SchemaField {
|
|
18
|
-
/** Expected type. Strings are always accepted as-is; number/boolean validate parsability. */
|
|
19
|
-
type: 'string' | 'number' | 'boolean';
|
|
20
|
-
/** If true, build fails when this var is missing or empty. Default: false. */
|
|
21
|
-
required?: boolean;
|
|
22
|
-
/** Optional regex pattern the value must match (applied to string representation). */
|
|
23
|
-
pattern?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type Schema = Record<string, SchemaField>;
|
|
27
|
-
|
|
28
|
-
export interface RC {
|
|
29
|
-
on_env?: (env: EnvData) => unknown;
|
|
30
|
-
js_override?: boolean;
|
|
31
|
-
/**
|
|
32
|
-
* Optional schema for build-time validation of env vars.
|
|
33
|
-
* Validated after `on_env` runs (so the hook can add/transform vars before validation).
|
|
34
|
-
*/
|
|
35
|
-
schema?: Schema;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export default async function resolve_env(env: EnvData, rc?: RC): Promise<EnvData> {
|
|
39
|
-
if (rc && rc.on_env) {
|
|
40
|
-
const patched_env = await rc.on_env(env);
|
|
41
|
-
return typeof patched_env === 'undefined' ? env : (patched_env as EnvData);
|
|
42
|
-
} else {
|
|
43
|
-
return env;
|
|
44
|
-
}
|
|
45
|
-
}
|
package/src/validate-env.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { EnvData, Schema } from './resolve-env';
|
|
2
|
-
|
|
3
|
-
const VALID_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Validate env data against a schema defined in `.rncurc.js`.
|
|
7
|
-
* Called after `on_env` so the hook can add/transform vars before validation.
|
|
8
|
-
*
|
|
9
|
-
* Throws with a human-readable error listing ALL failures at once
|
|
10
|
-
* (not just the first one), so users can fix everything in one pass.
|
|
11
|
-
*/
|
|
12
|
-
export function validate_env(env: EnvData, schema: Schema): void {
|
|
13
|
-
const errors: string[] = [];
|
|
14
|
-
|
|
15
|
-
// Validate all env key names are valid identifiers
|
|
16
|
-
for (const key of Object.keys(env)) {
|
|
17
|
-
if (!VALID_KEY_PATTERN.test(key)) {
|
|
18
|
-
errors.push(
|
|
19
|
-
`Invalid env key name: "${key}". Keys must start with a letter or underscore and contain only letters, numbers, and underscores.`
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Pre-compile all regex patterns once, before iterating over env values.
|
|
25
|
-
// This avoids re-compiling the same pattern for every validated key.
|
|
26
|
-
const compiled_patterns = new Map<string, RegExp>();
|
|
27
|
-
for (const [key, field] of Object.entries(schema)) {
|
|
28
|
-
if (field.pattern) {
|
|
29
|
-
try {
|
|
30
|
-
compiled_patterns.set(key, new RegExp(field.pattern));
|
|
31
|
-
} catch {
|
|
32
|
-
errors.push(`${key}: invalid regex pattern /${field.pattern}/`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
for (const [key, field] of Object.entries(schema)) {
|
|
38
|
-
const raw = env[key];
|
|
39
|
-
const missing = raw === undefined || raw === null || String(raw).trim() === '';
|
|
40
|
-
|
|
41
|
-
if (field.required && missing) {
|
|
42
|
-
errors.push(`Missing required env var: ${key}`);
|
|
43
|
-
continue; // can't type-check a missing value
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (missing) continue; // optional and not present → OK
|
|
47
|
-
|
|
48
|
-
const value = String(raw);
|
|
49
|
-
|
|
50
|
-
if (field.type === 'number' && isNaN(Number(value))) {
|
|
51
|
-
errors.push(`${key} must be a number, got "${value}"`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (field.type === 'boolean' && !['true', 'false', '1', '0'].includes(value.toLowerCase())) {
|
|
55
|
-
errors.push(`${key} must be a boolean (true/false/1/0), got "${value}"`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const pattern = compiled_patterns.get(key);
|
|
59
|
-
if (pattern && !pattern.test(value)) {
|
|
60
|
-
errors.push(`${key} does not match pattern /${field.pattern}/, got "${value}"`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (errors.length > 0) {
|
|
65
|
-
throw new Error(
|
|
66
|
-
`\n\n❌ react-native-config-ultimate: env validation failed:\n` +
|
|
67
|
-
errors.map((e) => ` • ${e}`).join('\n') +
|
|
68
|
-
'\n'
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
}
|
package/src/write-env.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
|
|
5
|
-
export type FileMap = Record<string, string>;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Atomically write all generated files.
|
|
9
|
-
*
|
|
10
|
-
* Strategy: write each file to a temp path first, then rename (atomic on POSIX).
|
|
11
|
-
* If any write fails, we abort before committing any renames so the project
|
|
12
|
-
* is never left in a partially-written state.
|
|
13
|
-
*
|
|
14
|
-
* On Windows, `fs.renameSync` across drives may fail — in that case we fall
|
|
15
|
-
* back to a direct `writeFileSync` (best-effort, still better than nothing).
|
|
16
|
-
*/
|
|
17
|
-
export default function write_env(files: FileMap): void {
|
|
18
|
-
const tmp_dir = os.tmpdir();
|
|
19
|
-
// Phase 1: write all content to temp files — if anything fails, no real files are touched.
|
|
20
|
-
const pending: Array<{ tmp: string; dest: string }> = [];
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
for (const dest of Object.keys(files)) {
|
|
24
|
-
// Ensure the destination directory exists (handles first-run and hoisted workspaces).
|
|
25
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
26
|
-
const tmp = path.join(tmp_dir, `rncu_${Date.now()}_${Math.random().toString(36).slice(2)}`);
|
|
27
|
-
fs.writeFileSync(tmp, files[dest], 'utf8');
|
|
28
|
-
pending.push({ tmp, dest });
|
|
29
|
-
}
|
|
30
|
-
} catch (err) {
|
|
31
|
-
// Clean up any temp files we already created.
|
|
32
|
-
for (const { tmp } of pending) {
|
|
33
|
-
try {
|
|
34
|
-
fs.unlinkSync(tmp);
|
|
35
|
-
} catch {
|
|
36
|
-
/* ignore */
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
throw new Error(
|
|
40
|
-
`[rncu] Failed to prepare output files: ${err instanceof Error ? err.message : String(err)}`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Phase 2: atomically rename temp → dest.
|
|
45
|
-
// We collect errors and rethrow at the end so the caller gets a clear message.
|
|
46
|
-
const rename_errors: string[] = [];
|
|
47
|
-
|
|
48
|
-
for (const { tmp, dest } of pending) {
|
|
49
|
-
try {
|
|
50
|
-
fs.renameSync(tmp, dest);
|
|
51
|
-
} catch {
|
|
52
|
-
// Cross-device rename (e.g. Windows different drives) — fall back to copy+delete.
|
|
53
|
-
try {
|
|
54
|
-
fs.copyFileSync(tmp, dest);
|
|
55
|
-
fs.unlinkSync(tmp);
|
|
56
|
-
} catch (copy_err) {
|
|
57
|
-
rename_errors.push(
|
|
58
|
-
`${dest}: ${copy_err instanceof Error ? copy_err.message : String(copy_err)}`
|
|
59
|
-
);
|
|
60
|
-
try {
|
|
61
|
-
fs.unlinkSync(tmp);
|
|
62
|
-
} catch {
|
|
63
|
-
/* ignore */
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (rename_errors.length > 0) {
|
|
70
|
-
throw new Error(
|
|
71
|
-
`[rncu] Failed to write output files:\n` + rename_errors.map((e) => ` • ${e}`).join('\n')
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
}
|