@philkrpp/phase-loader 0.1.1 → 0.1.3
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 +8 -4
- package/dist/cli.js +41 -9
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/dist/{run-QGYeawST.js → run-CkwwrycO.js} +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,8 @@ secrets: host, app ids, paths and keyring account names only.
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"require": ["DATABASE_URL"],
|
|
35
|
-
"allowLiteral": []
|
|
35
|
+
"allowLiteral": [],
|
|
36
|
+
"external": ["WORKER_TIER"]
|
|
36
37
|
}
|
|
37
38
|
```
|
|
38
39
|
|
|
@@ -45,6 +46,7 @@ secrets: host, app ids, paths and keyring account names only.
|
|
|
45
46
|
| `targets.<name>.personal.account` | Optional keyring account holding a personal app id or name; exported last (path `/`). |
|
|
46
47
|
| `require` | Keys the merged export must contain; otherwise loading fails and lists the missing names. |
|
|
47
48
|
| `allowLiteral` | Keys whose value may start with `${`. Any other value starting with `${` is an unresolved Phase reference and fails the load. |
|
|
49
|
+
| `external` | Names set or read outside the Phase `dev` export: Coolify-only values, CI, runtime, variables read by libraries (`OTEL_*`). `example --write` lists them in their own section; `check` never reports them. |
|
|
48
50
|
|
|
49
51
|
## Tokens
|
|
50
52
|
|
|
@@ -89,9 +91,11 @@ phase-loader check --offline # .env.example vs env reads in git-tracked files
|
|
|
89
91
|
phase-loader check # additionally vs the key names in Phase
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
`check` recognises `process.env.X`, `process.env["X"]`, `import.meta.env.X`, `os.environ["X"]
|
|
93
|
-
in tracked `.js/.ts/.vue/.py` files (not under `node_modules`), ignores runtime built-ins such as
|
|
94
|
-
names only and exits 1 on any undocumented or orphan name.
|
|
94
|
+
`check` recognises `process.env.X`, `process.env["X"]`, `import.meta.env.X`, `os.environ["X"]`, `os.environ.get("X")`
|
|
95
|
+
and `os.getenv("X")` in tracked `.js/.ts/.vue/.py` files (not under `node_modules`), ignores runtime built-ins such as
|
|
96
|
+
`NODE_ENV`, prints names only and exits 1 on any undocumented or orphan name. A name from `.env.example` that appears
|
|
97
|
+
quoted anywhere in that code (`optional('KEY')`, `env("KEY")`) counts as read, so env helpers cause no orphans; a new
|
|
98
|
+
key read only through such a helper is not reported as undocumented. The personal app is not part of `.env.example`.
|
|
95
99
|
|
|
96
100
|
## API
|
|
97
101
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as KEYRING_SERVICE, c as loadConfig, l as PhaseLoaderError, n as fetchLayers, s as writeKeyring, t as run } from "./run-
|
|
2
|
+
import { a as KEYRING_SERVICE, c as loadConfig, l as PhaseLoaderError, n as fetchLayers, s as writeKeyring, t as run } from "./run-CkwwrycO.js";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
@@ -35,7 +35,14 @@ function exampleNames(text) {
|
|
|
35
35
|
* Returns the text to print; values are never read into the output.
|
|
36
36
|
*/
|
|
37
37
|
async function example(opts = {}) {
|
|
38
|
-
const
|
|
38
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
39
|
+
const { dir, sections: phase } = await phaseNames(cwd);
|
|
40
|
+
const inPhase = new Set(phase.flatMap((s) => s.names));
|
|
41
|
+
const external = [...new Set(loadConfig(cwd).external)].filter((n) => !inPhase.has(n)).sort();
|
|
42
|
+
const sections = [...phase, {
|
|
43
|
+
label: "external (not loaded from Phase: Coolify, CI, runtime, libraries)",
|
|
44
|
+
names: external
|
|
45
|
+
}];
|
|
39
46
|
const text = sections.filter((s) => s.names.length > 0).map((s) => `# ${s.label}\n${s.names.map((n) => `${n}=`).join("\n")}\n`).join("\n");
|
|
40
47
|
const file = join(dir, EXAMPLE_FILE);
|
|
41
48
|
if (opts.write) {
|
|
@@ -54,8 +61,16 @@ const USAGE$1 = [
|
|
|
54
61
|
/process\.env\[\s*["']([A-Za-z_][A-Za-z0-9_]*)["']\s*\]/g,
|
|
55
62
|
/import\.meta\.env\.([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
56
63
|
/os\.environ\[\s*["']([A-Za-z_][A-Za-z0-9_]*)["']\s*\]/g,
|
|
57
|
-
/os\.getenv\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']/g
|
|
64
|
+
/os\.getenv\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']/g,
|
|
65
|
+
/os\.environ\.get\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']/g
|
|
58
66
|
];
|
|
67
|
+
/**
|
|
68
|
+
* A documented name quoted anywhere in tracked code (`optional('KEY')`, `env("KEY")`, a list of names) counts as
|
|
69
|
+
* read, so helpers around `process.env[name]` do not produce orphans.
|
|
70
|
+
* shortcut: undocumented detection still sees only the USAGE patterns, so a new key read only through a helper is
|
|
71
|
+
* not reported; upgrade with a configurable helper list if that bites.
|
|
72
|
+
*/
|
|
73
|
+
const QUOTED = /["'`]([A-Z_][A-Z0-9_]*)["'`]/g;
|
|
59
74
|
/** Provided by the runtime or by phase-loader itself, never documented in `.env.example`. */
|
|
60
75
|
const BUILT_IN = /* @__PURE__ */ new Set([
|
|
61
76
|
"NODE_ENV",
|
|
@@ -65,7 +80,17 @@ const BUILT_IN = /* @__PURE__ */ new Set([
|
|
|
65
80
|
"SSR",
|
|
66
81
|
"BASE_URL",
|
|
67
82
|
"CI",
|
|
68
|
-
"PHASE_ENV_LOADED"
|
|
83
|
+
"PHASE_ENV_LOADED",
|
|
84
|
+
"VITEST",
|
|
85
|
+
"PATH",
|
|
86
|
+
"HOME",
|
|
87
|
+
"USER",
|
|
88
|
+
"LOGNAME",
|
|
89
|
+
"SHELL",
|
|
90
|
+
"PWD",
|
|
91
|
+
"TMPDIR",
|
|
92
|
+
"TZ",
|
|
93
|
+
"LANG"
|
|
69
94
|
]);
|
|
70
95
|
function trackedFiles(dir) {
|
|
71
96
|
try {
|
|
@@ -84,6 +109,7 @@ function trackedFiles(dir) {
|
|
|
84
109
|
}
|
|
85
110
|
function usedNames(dir) {
|
|
86
111
|
const used = /* @__PURE__ */ new Set();
|
|
112
|
+
const quoted = /* @__PURE__ */ new Set();
|
|
87
113
|
for (const file of trackedFiles(dir)) {
|
|
88
114
|
if (!SOURCE.test(file) || file.split("/").includes("node_modules")) continue;
|
|
89
115
|
let text;
|
|
@@ -93,23 +119,29 @@ function usedNames(dir) {
|
|
|
93
119
|
continue;
|
|
94
120
|
}
|
|
95
121
|
for (const pattern of USAGE$1) for (const m of text.matchAll(pattern)) if (!BUILT_IN.has(m[1])) used.add(m[1]);
|
|
122
|
+
for (const m of text.matchAll(QUOTED)) quoted.add(m[1]);
|
|
96
123
|
}
|
|
97
|
-
return
|
|
124
|
+
return {
|
|
125
|
+
used,
|
|
126
|
+
quoted
|
|
127
|
+
};
|
|
98
128
|
}
|
|
99
129
|
/**
|
|
100
130
|
* Offline: names in `.env.example` against env reads in tracked source files (undocumented / orphan).
|
|
101
131
|
* Online: also against the key names in Phase (`dev`). Names only, never values. `ok` is false on any finding.
|
|
102
132
|
*/
|
|
103
133
|
async function check(opts = {}) {
|
|
104
|
-
const
|
|
134
|
+
const config = loadConfig(opts.cwd ?? process.cwd());
|
|
135
|
+
const dir = config.dir;
|
|
105
136
|
const file = join(dir, EXAMPLE_FILE);
|
|
106
137
|
if (!existsSync(file)) throw new PhaseLoaderError(`${EXAMPLE_FILE} not found in ${dir} (create it with "phase-loader example --write").`);
|
|
138
|
+
const external = new Set(config.external);
|
|
107
139
|
const documented = new Set(exampleNames(readFileSync(file, "utf8")));
|
|
108
|
-
const used = usedNames(dir);
|
|
109
|
-
const findings = [[`Used in code but undocumented in ${EXAMPLE_FILE}:`, [...used].filter((n) => !documented.has(n))], [`Orphan in ${EXAMPLE_FILE} (never read in tracked code):`, [...documented].filter((n) => !used.has(n))]];
|
|
140
|
+
const { used, quoted } = usedNames(dir);
|
|
141
|
+
const findings = [[`Used in code but undocumented in ${EXAMPLE_FILE}:`, [...used].filter((n) => !documented.has(n) && !external.has(n))], [`Orphan in ${EXAMPLE_FILE} (never read in tracked code):`, [...documented].filter((n) => !used.has(n) && !quoted.has(n) && !external.has(n))]];
|
|
110
142
|
if (!opts.offline) {
|
|
111
143
|
const inPhase = new Set((await phaseNames(opts.cwd ?? process.cwd())).sections.flatMap((s) => s.names));
|
|
112
|
-
findings.push([`In ${EXAMPLE_FILE} but not in Phase:`, [...documented].filter((n) => !inPhase.has(n))], [`In Phase but not in ${EXAMPLE_FILE}:`, [...inPhase].filter((n) => !documented.has(n))]);
|
|
144
|
+
findings.push([`In ${EXAMPLE_FILE} but not in Phase:`, [...documented].filter((n) => !inPhase.has(n) && !external.has(n))], [`In Phase but not in ${EXAMPLE_FILE}:`, [...inPhase].filter((n) => !documented.has(n))]);
|
|
113
145
|
}
|
|
114
146
|
const lines = findings.filter(([, names]) => names.length > 0).flatMap(([title, names]) => [title, ` ${names.sort().join(", ")}`]);
|
|
115
147
|
return lines.length > 0 ? {
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,11 @@ interface PhaseConfig {
|
|
|
26
26
|
require: string[];
|
|
27
27
|
/** Keys whose value may start with `${` (not a Phase reference). */
|
|
28
28
|
allowLiteral: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Names read or set outside the Phase `dev` export (Coolify-only values, CI, runtime, libraries such as OTEL_*):
|
|
31
|
+
* listed in their own `.env.example` section and never reported by `check`.
|
|
32
|
+
*/
|
|
33
|
+
external: string[];
|
|
29
34
|
}
|
|
30
35
|
/** Finds the nearest `phase.config.json` from `cwd` upwards, validates and returns it. */
|
|
31
36
|
export declare function loadConfig(cwd: string): PhaseConfig;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { l as PhaseLoaderError, c as loadConfig, r as loadPhaseEnv, i as loadPhaseEnvSync, o as readToken, t as run } from "./run-
|
|
1
|
+
export { l as PhaseLoaderError, c as loadConfig, r as loadPhaseEnv, i as loadPhaseEnvSync, o as readToken, t as run } from "./run-CkwwrycO.js";
|
|
@@ -84,7 +84,8 @@ function parseConfig(file, dir) {
|
|
|
84
84
|
host,
|
|
85
85
|
targets,
|
|
86
86
|
require: keys(raw.require, "require"),
|
|
87
|
-
allowLiteral: keys(raw.allowLiteral, "allowLiteral")
|
|
87
|
+
allowLiteral: keys(raw.allowLiteral, "allowLiteral"),
|
|
88
|
+
external: keys(raw.external, "external")
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
function isObject$1(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@philkrpp/phase-loader",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Load env values from a self-hosted Phase secrets manager into a child process or the current process, with layered apps/paths and OS keyring tokens.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|