@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 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"]` and `os.getenv("X")`
93
- in tracked `.js/.ts/.vue/.py` files (not under `node_modules`), ignores runtime built-ins such as `NODE_ENV`, prints
94
- names only and exits 1 on any undocumented or orphan name. The personal app is not part of `.env.example`.
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-QGYeawST.js";
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 { dir, sections } = await phaseNames(opts.cwd ?? process.cwd());
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 used;
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 dir = loadConfig(opts.cwd ?? process.cwd()).dir;
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-QGYeawST.js";
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.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": {