@penvhq/cli 0.5.0 → 0.7.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/dist/index.cjs +327 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +327 -52
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -368,19 +368,19 @@ function refsFrom(files) {
|
|
|
368
368
|
|
|
369
369
|
// src/schema.ts
|
|
370
370
|
var import_core4 = require("@penvhq/core");
|
|
371
|
-
function defOf(
|
|
372
|
-
if (typeof
|
|
371
|
+
function defOf(node2) {
|
|
372
|
+
if (typeof node2 !== "object" || node2 === null) {
|
|
373
373
|
return void 0;
|
|
374
374
|
}
|
|
375
|
-
const def =
|
|
375
|
+
const def = node2.def;
|
|
376
376
|
return typeof def === "object" && def !== null ? def : void 0;
|
|
377
377
|
}
|
|
378
|
-
function typeOf(
|
|
379
|
-
const type = defOf(
|
|
378
|
+
function typeOf(node2) {
|
|
379
|
+
const type = defOf(node2)?.type;
|
|
380
380
|
return typeof type === "string" ? type : void 0;
|
|
381
381
|
}
|
|
382
|
-
function unwrap(
|
|
383
|
-
let current =
|
|
382
|
+
function unwrap(node2) {
|
|
383
|
+
let current = node2;
|
|
384
384
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
385
385
|
const inner = defOf(current)?.innerType;
|
|
386
386
|
if (inner === void 0) {
|
|
@@ -390,38 +390,38 @@ function unwrap(node) {
|
|
|
390
390
|
}
|
|
391
391
|
return current;
|
|
392
392
|
}
|
|
393
|
-
function shapeOf(
|
|
394
|
-
if (typeOf(
|
|
393
|
+
function shapeOf(node2) {
|
|
394
|
+
if (typeOf(node2) !== "object") {
|
|
395
395
|
return void 0;
|
|
396
396
|
}
|
|
397
|
-
const shape =
|
|
397
|
+
const shape = node2.shape;
|
|
398
398
|
return typeof shape === "object" && shape !== null ? shape : void 0;
|
|
399
399
|
}
|
|
400
400
|
function lookup(root, path) {
|
|
401
|
-
let
|
|
401
|
+
let node2 = unwrap(root);
|
|
402
402
|
for (const key of path) {
|
|
403
|
-
const shape = shapeOf(
|
|
403
|
+
const shape = shapeOf(node2);
|
|
404
404
|
if (shape === void 0) {
|
|
405
405
|
return { kind: "unknown" };
|
|
406
406
|
}
|
|
407
407
|
if (!Object.hasOwn(shape, key)) {
|
|
408
408
|
return { kind: "absent" };
|
|
409
409
|
}
|
|
410
|
-
|
|
410
|
+
node2 = unwrap(shape[key]);
|
|
411
411
|
}
|
|
412
|
-
return { kind: "found", node };
|
|
412
|
+
return { kind: "found", node: node2 };
|
|
413
413
|
}
|
|
414
|
-
function minLengthOf(
|
|
415
|
-
if (typeOf(
|
|
414
|
+
function minLengthOf(node2) {
|
|
415
|
+
if (typeOf(node2) !== "string") {
|
|
416
416
|
return void 0;
|
|
417
417
|
}
|
|
418
|
-
const min =
|
|
418
|
+
const min = node2.minLength;
|
|
419
419
|
return typeof min === "number" ? min : void 0;
|
|
420
420
|
}
|
|
421
421
|
var ABSENCE_PERMITTED = /* @__PURE__ */ new Set(["optional", "default", "catch", "prefault"]);
|
|
422
422
|
var ABSENCE_REFUSED = /* @__PURE__ */ new Set(["nullable", "nonoptional", "readonly"]);
|
|
423
|
-
function permitsAbsence(
|
|
424
|
-
let current =
|
|
423
|
+
function permitsAbsence(node2) {
|
|
424
|
+
let current = node2;
|
|
425
425
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
426
426
|
const type = typeOf(current);
|
|
427
427
|
if (type !== void 0 && ABSENCE_PERMITTED.has(type)) {
|
|
@@ -438,13 +438,13 @@ function permitsAbsence(node) {
|
|
|
438
438
|
}
|
|
439
439
|
return void 0;
|
|
440
440
|
}
|
|
441
|
-
function leaves(
|
|
442
|
-
const own = permitsAbsence(
|
|
441
|
+
function leaves(node2, path, inherited, out2) {
|
|
442
|
+
const own = permitsAbsence(node2);
|
|
443
443
|
const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
|
|
444
|
-
const shape = shapeOf(unwrap(
|
|
444
|
+
const shape = shapeOf(unwrap(node2));
|
|
445
445
|
if (shape === void 0) {
|
|
446
446
|
if (path.length > 0) {
|
|
447
|
-
out2.push({ path, absencePermitted, node });
|
|
447
|
+
out2.push({ path, absencePermitted, node: node2 });
|
|
448
448
|
}
|
|
449
449
|
return;
|
|
450
450
|
}
|
|
@@ -460,8 +460,8 @@ function declaredLeaves(schema) {
|
|
|
460
460
|
leaves(schema, [], false, out2);
|
|
461
461
|
return out2;
|
|
462
462
|
}
|
|
463
|
-
function defaultValueOf(
|
|
464
|
-
let current =
|
|
463
|
+
function defaultValueOf(node2) {
|
|
464
|
+
let current = node2;
|
|
465
465
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
466
466
|
const def = defOf(current);
|
|
467
467
|
if (def === void 0) {
|
|
@@ -1111,18 +1111,18 @@ function place(root, path, value) {
|
|
|
1111
1111
|
if (leaf === void 0) {
|
|
1112
1112
|
return;
|
|
1113
1113
|
}
|
|
1114
|
-
let
|
|
1114
|
+
let node2 = root;
|
|
1115
1115
|
for (const key of path.slice(0, -1)) {
|
|
1116
|
-
const existing =
|
|
1116
|
+
const existing = node2[key];
|
|
1117
1117
|
if (typeof existing === "object" && existing !== null) {
|
|
1118
|
-
|
|
1118
|
+
node2 = existing;
|
|
1119
1119
|
continue;
|
|
1120
1120
|
}
|
|
1121
1121
|
const child = {};
|
|
1122
|
-
|
|
1123
|
-
|
|
1122
|
+
node2[key] = child;
|
|
1123
|
+
node2 = child;
|
|
1124
1124
|
}
|
|
1125
|
-
|
|
1125
|
+
node2[leaf] = value;
|
|
1126
1126
|
}
|
|
1127
1127
|
function isZodType(value) {
|
|
1128
1128
|
return typeof value === "object" && value !== null && "safeParse" in value && typeof value.safeParse === "function";
|
|
@@ -2775,6 +2775,8 @@ var import_node_path5 = require("path");
|
|
|
2775
2775
|
var import_core14 = require("@penvhq/core");
|
|
2776
2776
|
var SIGNATURES = [
|
|
2777
2777
|
{ name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
|
|
2778
|
+
{ name: "SvelteKit", packages: ["@sveltejs/kit"], publicPrefixes: ["PUBLIC_"] },
|
|
2779
|
+
{ name: "Nuxt", packages: ["nuxt"], publicPrefixes: ["NUXT_PUBLIC_"] },
|
|
2778
2780
|
{
|
|
2779
2781
|
name: "TanStack Start",
|
|
2780
2782
|
packages: ["@tanstack/react-start", "@tanstack/start"],
|
|
@@ -2801,7 +2803,7 @@ function occupied(cwd, relative3) {
|
|
|
2801
2803
|
return (0, import_node_fs2.existsSync)(file) && !exportsSchema(file);
|
|
2802
2804
|
}
|
|
2803
2805
|
function schemaFileFor(cwd) {
|
|
2804
|
-
const dir = (
|
|
2806
|
+
const dir = srcPrefix(cwd);
|
|
2805
2807
|
const preferred = `${dir}env.ts`;
|
|
2806
2808
|
if (!occupied(cwd, preferred)) {
|
|
2807
2809
|
return { file: preferred };
|
|
@@ -2812,6 +2814,9 @@ function schemaFileFor(cwd) {
|
|
|
2812
2814
|
}
|
|
2813
2815
|
return { file: import_core14.DEFAULT_SCHEMA_FILE, displaced: preferred };
|
|
2814
2816
|
}
|
|
2817
|
+
function srcPrefix(cwd) {
|
|
2818
|
+
return (0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "src")) ? "src/" : "";
|
|
2819
|
+
}
|
|
2815
2820
|
function dependenciesOf(cwd) {
|
|
2816
2821
|
const manifest = manifestOf(cwd);
|
|
2817
2822
|
if (manifest === void 0) {
|
|
@@ -2848,17 +2853,23 @@ function detectFramework(cwd) {
|
|
|
2848
2853
|
}
|
|
2849
2854
|
for (const signature of SIGNATURES) {
|
|
2850
2855
|
if (signature.packages.some((name) => dependencies.has(name))) {
|
|
2851
|
-
|
|
2852
|
-
return {
|
|
2853
|
-
name: signature.name,
|
|
2854
|
-
schemaFile: schema.file,
|
|
2855
|
-
...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
|
|
2856
|
-
publicPrefixes: signature.publicPrefixes
|
|
2857
|
-
};
|
|
2856
|
+
return detectedFrom(cwd, signature.name, signature.publicPrefixes);
|
|
2858
2857
|
}
|
|
2859
2858
|
}
|
|
2859
|
+
if ((0, import_node_fs2.existsSync)((0, import_node_path5.join)(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
|
|
2860
|
+
return detectedFrom(cwd, "Bun", []);
|
|
2861
|
+
}
|
|
2860
2862
|
return void 0;
|
|
2861
2863
|
}
|
|
2864
|
+
function detectedFrom(cwd, name, publicPrefixes) {
|
|
2865
|
+
const schema = schemaFileFor(cwd);
|
|
2866
|
+
return {
|
|
2867
|
+
name,
|
|
2868
|
+
schemaFile: schema.file,
|
|
2869
|
+
...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
|
|
2870
|
+
publicPrefixes
|
|
2871
|
+
};
|
|
2872
|
+
}
|
|
2862
2873
|
var DEFAULT_ALIAS = "@env";
|
|
2863
2874
|
var IMPORTS_ALIAS = "#env";
|
|
2864
2875
|
function detectAlias(cwd) {
|
|
@@ -2876,6 +2887,145 @@ var import_node_path6 = require("path");
|
|
|
2876
2887
|
var import_promises = require("readline/promises");
|
|
2877
2888
|
var import_core15 = require("@penvhq/core");
|
|
2878
2889
|
var import_citty9 = require("citty");
|
|
2890
|
+
|
|
2891
|
+
// src/seams.ts
|
|
2892
|
+
function nextjs({ alias, srcDir }) {
|
|
2893
|
+
return {
|
|
2894
|
+
kind: "scaffold",
|
|
2895
|
+
file: `${srcDir}instrumentation.ts`,
|
|
2896
|
+
content: `// This is instrumentation.ts, Next.js's own startup hook \u2014 Next runs it once,
|
|
2897
|
+
// before any of your app code, on every server boot. penv put one line here so
|
|
2898
|
+
// your config is in process.env before the first library reads it. The filename
|
|
2899
|
+
// is Next's requirement (it only finds \`instrumentation.ts\`), so keep it as-is.
|
|
2900
|
+
export async function register() {
|
|
2901
|
+
// Next also calls register on the Edge runtime, where penv cannot read the
|
|
2902
|
+
// filesystem \u2014 so inject runs on the Node runtime only.
|
|
2903
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
2904
|
+
await import("${alias}");
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
`,
|
|
2908
|
+
ifPresent: `Add to your existing \`${srcDir}instrumentation.ts\`, inside \`register()\` and first:
|
|
2909
|
+
if (process.env.NEXT_RUNTIME === "nodejs") { await import("${alias}"); }`,
|
|
2910
|
+
notes: []
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
function sveltekit({ alias }) {
|
|
2914
|
+
return {
|
|
2915
|
+
kind: "scaffold",
|
|
2916
|
+
// SvelteKit's hooks module is always under `src/`, whatever the schema path.
|
|
2917
|
+
file: "src/hooks.server.ts",
|
|
2918
|
+
content: `// This is hooks.server.ts, SvelteKit's server-startup module \u2014 it loads before
|
|
2919
|
+
// your routes handle a request. penv put this import first so your config is in
|
|
2920
|
+
// process.env before any library reads it. Keep it the first line; the name and
|
|
2921
|
+
// location are SvelteKit's, not penv's.
|
|
2922
|
+
import "${alias}";
|
|
2923
|
+
`,
|
|
2924
|
+
ifPresent: `Add \`import "${alias}";\` as the FIRST line of src/hooks.server.ts, above every other import.`,
|
|
2925
|
+
notes: [
|
|
2926
|
+
`SvelteKit resolves app aliases through \`kit.alias\` \u2014 make sure \`${alias}\` is registered in svelte.config.js, not only tsconfig.`
|
|
2927
|
+
]
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
function nuxt({ alias }) {
|
|
2931
|
+
return {
|
|
2932
|
+
kind: "scaffold",
|
|
2933
|
+
// The `0.` prefix is load-bearing: Nitro sorts plugin filenames as strings,
|
|
2934
|
+
// so it must sort first to run before any other plugin reads process.env.
|
|
2935
|
+
file: "server/plugins/0.penv.ts",
|
|
2936
|
+
content: `// This is a Nitro server plugin (Nuxt's server layer runs it once at startup,
|
|
2937
|
+
// before any middleware or request handler). penv put the import here so your
|
|
2938
|
+
// config is in process.env before a library reads it. The \`0.\` prefix keeps this
|
|
2939
|
+
// plugin first \u2014 Nitro sorts plugin filenames as text \u2014 so don't rename it.
|
|
2940
|
+
import "${alias}";
|
|
2941
|
+
|
|
2942
|
+
// Nitro requires every plugin file to default-export a function; the injection
|
|
2943
|
+
// above already ran at import, so this body is intentionally empty.
|
|
2944
|
+
export default defineNitroPlugin(() => {});
|
|
2945
|
+
`,
|
|
2946
|
+
ifPresent: `Create server/plugins/0.penv.ts with \`import "${alias}";\` and \`export default defineNitroPlugin(() => {});\`.`,
|
|
2947
|
+
notes: [
|
|
2948
|
+
`Wire \`${alias}\` for the Nitro (server) build so it never leaks into the client bundle.`
|
|
2949
|
+
]
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
function bun({ alias }) {
|
|
2953
|
+
return {
|
|
2954
|
+
kind: "scaffold",
|
|
2955
|
+
file: ".penv/preload.ts",
|
|
2956
|
+
content: `// Bun evaluates this file before your entry point, because it is listed under
|
|
2957
|
+
// \`preload\` in bunfig.toml. Running it injects your config into process.env
|
|
2958
|
+
// before any library reads it.
|
|
2959
|
+
import "${alias}";
|
|
2960
|
+
`,
|
|
2961
|
+
ifPresent: `Keep .penv/preload.ts as \`import "${alias}";\`.`,
|
|
2962
|
+
notes: [`\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`],
|
|
2963
|
+
// The preload file is inert until bunfig.toml points at it — so penv writes
|
|
2964
|
+
// that too, both for the runtime and for `bun test`. Only when there is no
|
|
2965
|
+
// bunfig to overwrite; an existing one is the user's, and penv prints instead.
|
|
2966
|
+
also: {
|
|
2967
|
+
file: "bunfig.toml",
|
|
2968
|
+
content: 'preload = ["./.penv/preload.ts"]\n\n[test]\npreload = ["./.penv/preload.ts"]\n',
|
|
2969
|
+
ifPresent: 'Add "./.penv/preload.ts" to the `preload` array in bunfig.toml \u2014 and mirror it under `[test]` so `bun test` injects too.'
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
function node({ alias, schemaFile }) {
|
|
2974
|
+
const builtSchema = `./${schemaFile.replace(/\.ts$/, ".js")}`;
|
|
2975
|
+
const runtimeSpecifier = alias.startsWith("#") ? alias : builtSchema;
|
|
2976
|
+
return {
|
|
2977
|
+
kind: "instruct",
|
|
2978
|
+
instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
|
|
2979
|
+
code, so place the injection yourself, one of two ways (both run first):
|
|
2980
|
+
\u2022 Preload at launch \u2014 add to your start script: node --import "${runtimeSpecifier}" your-entry.js
|
|
2981
|
+
\u2022 Or make it the FIRST line of your entry file: import "${runtimeSpecifier}";
|
|
2982
|
+
` + (alias.startsWith("#") ? "" : `Note: \`${alias}\` is a tsconfig alias that Node does not resolve at runtime \u2014 the paths above point at the built schema module instead.
|
|
2983
|
+
`)
|
|
2984
|
+
};
|
|
2985
|
+
}
|
|
2986
|
+
function tanstack({ alias, srcDir }) {
|
|
2987
|
+
return {
|
|
2988
|
+
kind: "instruct",
|
|
2989
|
+
instruction: `Add \`import "${alias}";\` as the FIRST line of ${srcDir}server.ts (TanStack Start's
|
|
2990
|
+
server entry), above the \`@tanstack/react-start/server-entry\` import \u2014 nothing may
|
|
2991
|
+
import before it. If you have no server.ts yet, create it from TanStack's server-entry
|
|
2992
|
+
template and put that import first.`
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2995
|
+
function astro({ alias }) {
|
|
2996
|
+
return {
|
|
2997
|
+
kind: "instruct",
|
|
2998
|
+
instruction: `Astro has no single pre-app hook. Add a small integration to astro.config that
|
|
2999
|
+
injects the import into every SSR page, then cover the routes it cannot reach:
|
|
3000
|
+
\u2022 In astro.config integrations, add:
|
|
3001
|
+
{ name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>
|
|
3002
|
+
injectScript("page-ssr", 'import "${alias}";') } }
|
|
3003
|
+
\u2022 Also add \`import "${alias}";\` as the first line of src/middleware.ts and of any
|
|
3004
|
+
endpoint route (src/pages/*.ts) that imports a library reading process.env.
|
|
3005
|
+
Injection only applies under a server (on-demand) adapter, not a static build.`
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
function viteSpa(_context) {
|
|
3009
|
+
return {
|
|
3010
|
+
kind: "none",
|
|
3011
|
+
reason: "This is a client-only app \u2014 nothing reads process.env at runtime, so injection does not apply. Read client config through import.meta.env (only VITE_-prefixed vars, which ship to the browser and must not hold secrets). If you add a server later, place the injection there."
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
var SEAMS = {
|
|
3015
|
+
"Next.js": nextjs,
|
|
3016
|
+
SvelteKit: sveltekit,
|
|
3017
|
+
Nuxt: nuxt,
|
|
3018
|
+
Bun: bun,
|
|
3019
|
+
"TanStack Start": tanstack,
|
|
3020
|
+
Astro: astro,
|
|
3021
|
+
Vite: viteSpa
|
|
3022
|
+
};
|
|
3023
|
+
function seamFor(framework, context) {
|
|
3024
|
+
const build = framework === void 0 ? node : SEAMS[framework] ?? node;
|
|
3025
|
+
return build(context);
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// src/commands/init.ts
|
|
2879
3029
|
var CONFIG_FILE = "penv.config.ts";
|
|
2880
3030
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
2881
3031
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -2885,6 +3035,7 @@ var IMPORTS_PREFIX = "#";
|
|
|
2885
3035
|
var PACKAGE_FILE = "package.json";
|
|
2886
3036
|
var DEFAULT_DECISIONS = {
|
|
2887
3037
|
environments: [],
|
|
3038
|
+
inject: false,
|
|
2888
3039
|
schemaFile: import_core15.DEFAULT_SCHEMA_FILE,
|
|
2889
3040
|
publicPrefixes: [],
|
|
2890
3041
|
alias: DEFAULT_ALIAS
|
|
@@ -3003,6 +3154,9 @@ function planInit(root, flags = {}) {
|
|
|
3003
3154
|
detected,
|
|
3004
3155
|
decisions: {
|
|
3005
3156
|
environments,
|
|
3157
|
+
// Injection is a choice about the app's needs, not something on disk — so
|
|
3158
|
+
// the plan defaults it off, and only the interactive prompt turns it on.
|
|
3159
|
+
inject: false,
|
|
3006
3160
|
schemaFile,
|
|
3007
3161
|
publicPrefixes: declared?.publicPrefixes ?? detected?.publicPrefixes ?? [],
|
|
3008
3162
|
alias
|
|
@@ -3053,12 +3207,30 @@ async function promptForDecisions(plan2, io) {
|
|
|
3053
3207
|
if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
|
|
3054
3208
|
return void 0;
|
|
3055
3209
|
}
|
|
3056
|
-
|
|
3210
|
+
const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
|
|
3211
|
+
return { ...plan2.decisions, environments, inject };
|
|
3212
|
+
}
|
|
3213
|
+
function seamKindFor(plan2) {
|
|
3214
|
+
return seamFor(plan2.detected?.name, {
|
|
3215
|
+
alias: plan2.decisions.alias,
|
|
3216
|
+
srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : "",
|
|
3217
|
+
schemaFile: plan2.decisions.schemaFile
|
|
3218
|
+
}).kind;
|
|
3219
|
+
}
|
|
3220
|
+
async function askInject(io) {
|
|
3221
|
+
io.write("");
|
|
3222
|
+
io.write("Also inject your validated config into process.env?");
|
|
3223
|
+
io.write(" y libraries that read process.env directly (WorkOS, Prisma\u2026) just work");
|
|
3224
|
+
io.write(' n config stays available only through import { env } from "@env"');
|
|
3225
|
+
const answer = (await io.ask(prompt("inject", "y/N"))).trim().toLowerCase();
|
|
3226
|
+
return answer === "y" || answer === "yes";
|
|
3057
3227
|
}
|
|
3058
3228
|
var EMPTY_SCHEMA_BODY = " // One key per parameter, e.g. `databaseUrl: z.url(),`. Nesting a key nests\n // the parameter: `redis: z.object({ password: z.string() })` is redis/password.";
|
|
3059
3229
|
var DRAFT_HEADER = "// DRAFT \u2014 generated by `penv import` from one sample of each value, and yours\n// to correct. Single-sample inference cannot know that a boolean seen as `true`\n// must also accept `1`/`0`, or that a string is really a URL. penv scaffolds\n// this file once and never regenerates it, so edits here are safe.\n";
|
|
3060
|
-
function renderSchemaModule(fields, draft) {
|
|
3230
|
+
function renderSchemaModule(fields, draft, inject = false) {
|
|
3061
3231
|
const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
|
|
3232
|
+
const loadComment = inject ? "// The loaded, validated values for the current environment. Import this in app\n// code. `inject: true` also copies the values into process.env, for libraries\n// that read process.env directly (WorkOS, Prisma) instead of importing @env.\n" : "// The loaded, validated values for the current environment. Import this in app\n// code. Importing it loads configuration and throws (naming the parameter and\n// environment) if anything required is missing or invalid.\n";
|
|
3233
|
+
const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
|
|
3062
3234
|
return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
|
|
3063
3235
|
import { load } from "@penvhq/penv";
|
|
3064
3236
|
|
|
@@ -3068,11 +3240,7 @@ export const schema = z.object({
|
|
|
3068
3240
|
${body}
|
|
3069
3241
|
});
|
|
3070
3242
|
|
|
3071
|
-
|
|
3072
|
-
// code. Importing it loads configuration and throws (naming the parameter and
|
|
3073
|
-
// environment) if anything required is missing or invalid.
|
|
3074
|
-
export const env = load(schema);
|
|
3075
|
-
|
|
3243
|
+
` + loadComment + loadCall + `
|
|
3076
3244
|
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3077
3245
|
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3078
3246
|
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
@@ -3364,7 +3532,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
3364
3532
|
};
|
|
3365
3533
|
}
|
|
3366
3534
|
(0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
3367
|
-
(0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft), "utf8");
|
|
3535
|
+
(0, import_node_fs3.writeFileSync)(file, renderSchemaModule(fields, draft, decisions.inject), "utf8");
|
|
3368
3536
|
return {
|
|
3369
3537
|
target: "schema",
|
|
3370
3538
|
action: "created",
|
|
@@ -3441,23 +3609,119 @@ function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3441
3609
|
text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
|
|
3442
3610
|
};
|
|
3443
3611
|
}
|
|
3444
|
-
|
|
3445
|
-
|
|
3612
|
+
var INJECT_MIN_VERSION = "0.6.0";
|
|
3613
|
+
function outdatedRuntimeWarning(root) {
|
|
3614
|
+
const version = installedPenvVersion(root);
|
|
3615
|
+
if (version === void 0 || !isBelow(version, INJECT_MIN_VERSION)) {
|
|
3616
|
+
return void 0;
|
|
3617
|
+
}
|
|
3618
|
+
return `Injection needs @penvhq/penv ${INJECT_MIN_VERSION}+ \u2014 this project has ${version}, whose \`load\` ignores \`{ inject: true }\`. Upgrade, or process.env stays empty.`;
|
|
3619
|
+
}
|
|
3620
|
+
function installedPenvVersion(root) {
|
|
3621
|
+
const file = (0, import_node_path6.join)(root, "node_modules", "@penvhq", "penv", "package.json");
|
|
3622
|
+
if (!(0, import_node_fs3.existsSync)(file)) {
|
|
3623
|
+
return void 0;
|
|
3624
|
+
}
|
|
3625
|
+
try {
|
|
3626
|
+
const version = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8")).version;
|
|
3627
|
+
return typeof version === "string" ? version : void 0;
|
|
3628
|
+
} catch {
|
|
3629
|
+
return void 0;
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
function releaseTriple(version) {
|
|
3633
|
+
const parts = (version.split("-")[0] ?? "").split(".");
|
|
3634
|
+
const at = (i) => Number.parseInt(parts[i] ?? "0", 10) || 0;
|
|
3635
|
+
return [at(0), at(1), at(2)];
|
|
3636
|
+
}
|
|
3637
|
+
function isBelow(a, b) {
|
|
3638
|
+
const [am, an, ap] = releaseTriple(a);
|
|
3639
|
+
const [bm, bn, bp] = releaseTriple(b);
|
|
3640
|
+
if (am !== bm) return am < bm;
|
|
3641
|
+
if (an !== bn) return an < bn;
|
|
3642
|
+
return ap < bp;
|
|
3643
|
+
}
|
|
3644
|
+
function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
|
|
3645
|
+
if (!decisions.inject) {
|
|
3646
|
+
return void 0;
|
|
3647
|
+
}
|
|
3648
|
+
const seam = seamFor(framework, {
|
|
3649
|
+
alias: decisions.alias,
|
|
3650
|
+
srcDir: srcPrefix(root),
|
|
3651
|
+
schemaFile: decisions.schemaFile
|
|
3652
|
+
});
|
|
3653
|
+
const outdated = outdatedRuntimeWarning(root);
|
|
3654
|
+
if (seam.kind === "none") {
|
|
3655
|
+
return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
|
|
3656
|
+
}
|
|
3657
|
+
if (seam.kind === "instruct") {
|
|
3658
|
+
return {
|
|
3659
|
+
target: "seam",
|
|
3660
|
+
action: "info",
|
|
3661
|
+
text: "Place the injection seam",
|
|
3662
|
+
note: withWarning(seam.instruction, outdated)
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3665
|
+
const alsoNote = writeAlso(root, seam.also);
|
|
3666
|
+
const file = (0, import_node_path6.join)(root, ...seam.file.split("/"));
|
|
3667
|
+
const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
|
|
3668
|
+
const notes = baseNotes.length === 0 ? "" : `
|
|
3669
|
+
${baseNotes.map((n) => ` ${n}`).join("\n")}`;
|
|
3670
|
+
if ((0, import_node_fs3.existsSync)(file)) {
|
|
3671
|
+
return {
|
|
3672
|
+
target: "seam",
|
|
3673
|
+
action: "info",
|
|
3674
|
+
text: `Add the injection seam to ${seam.file}`,
|
|
3675
|
+
note: withWarning(`${seam.ifPresent}${notes}`, outdated)
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
3679
|
+
(0, import_node_fs3.writeFileSync)(file, seam.content, "utf8");
|
|
3680
|
+
return {
|
|
3681
|
+
target: "seam",
|
|
3682
|
+
action: outdated === void 0 ? "created" : "info",
|
|
3683
|
+
text: `Wrote ${seam.file} (runs the injection before your app)`,
|
|
3684
|
+
...notes === "" && outdated === void 0 ? {} : { note: withWarning(notes.trimStart(), outdated) }
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
function writeAlso(root, also) {
|
|
3688
|
+
if (also === void 0) {
|
|
3689
|
+
return void 0;
|
|
3690
|
+
}
|
|
3691
|
+
const file = (0, import_node_path6.join)(root, ...also.file.split("/"));
|
|
3692
|
+
if ((0, import_node_fs3.existsSync)(file)) {
|
|
3693
|
+
return also.ifPresent;
|
|
3694
|
+
}
|
|
3695
|
+
(0, import_node_fs3.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true });
|
|
3696
|
+
(0, import_node_fs3.writeFileSync)(file, also.content, "utf8");
|
|
3697
|
+
return `Wrote ${also.file} to register it.`;
|
|
3698
|
+
}
|
|
3699
|
+
function withWarning(note, warning) {
|
|
3700
|
+
if (warning === void 0) {
|
|
3701
|
+
return note;
|
|
3702
|
+
}
|
|
3703
|
+
return note === "" ? warning : `${note}
|
|
3704
|
+
\u26A0 ${warning}`;
|
|
3705
|
+
}
|
|
3706
|
+
function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
|
|
3707
|
+
const steps = [
|
|
3446
3708
|
ensurePenvDir(root),
|
|
3447
3709
|
writeSchemaFile(root, fields, draft, decisions),
|
|
3448
3710
|
writeConfigFile(root, decisions),
|
|
3449
3711
|
writeTsconfigAlias(root, decisions),
|
|
3450
3712
|
writeGitignore(root, decisions)
|
|
3451
3713
|
];
|
|
3714
|
+
const seam = writeSeam(root, decisions, framework);
|
|
3715
|
+
return seam === void 0 ? steps : [...steps, seam];
|
|
3452
3716
|
}
|
|
3453
3717
|
function runInit(options) {
|
|
3454
3718
|
const root = (0, import_node_path6.resolve)(options.cwd);
|
|
3455
3719
|
const decisions = options.decisions ?? planInit(root).decisions;
|
|
3456
|
-
return { root, decisions, steps: scaffold(root, [], false, decisions) };
|
|
3720
|
+
return { root, decisions, steps: scaffold(root, [], false, decisions, options.framework) };
|
|
3457
3721
|
}
|
|
3458
3722
|
function renderInit(result2) {
|
|
3459
3723
|
const steps = result2.steps.map((step) => {
|
|
3460
|
-
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
3724
|
+
const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
|
|
3461
3725
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
3462
3726
|
});
|
|
3463
3727
|
return [
|
|
@@ -3520,7 +3784,15 @@ var initCommand = (0, import_citty9.defineCommand)({
|
|
|
3520
3784
|
if (!asked) {
|
|
3521
3785
|
write([...plan2.notes, ""]);
|
|
3522
3786
|
}
|
|
3523
|
-
write(
|
|
3787
|
+
write(
|
|
3788
|
+
renderInit(
|
|
3789
|
+
runInit({
|
|
3790
|
+
cwd: root,
|
|
3791
|
+
decisions,
|
|
3792
|
+
...plan2.detected && { framework: plan2.detected.name }
|
|
3793
|
+
})
|
|
3794
|
+
)
|
|
3795
|
+
);
|
|
3524
3796
|
});
|
|
3525
3797
|
}
|
|
3526
3798
|
});
|
|
@@ -3690,6 +3962,9 @@ function environmentNamed(file, explicit) {
|
|
|
3690
3962
|
function decisionsOf(config, cwd) {
|
|
3691
3963
|
return {
|
|
3692
3964
|
environments: config.environments,
|
|
3965
|
+
// `import` re-scaffolds env.ts for an existing project; injection is an init
|
|
3966
|
+
// choice, so it is not turned on here.
|
|
3967
|
+
inject: false,
|
|
3693
3968
|
schemaFile: (0, import_core16.schemaFileOf)(config),
|
|
3694
3969
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
3695
3970
|
alias: detectAlias(cwd)
|