@penvhq/cli 0.5.0 → 0.6.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 +244 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +244 -48
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.d.cts
CHANGED
|
@@ -250,14 +250,18 @@ declare function runGet(options: GetOptions): Promise<string>;
|
|
|
250
250
|
declare function runExplain(options: GetOptions): Promise<GetExplanation>;
|
|
251
251
|
|
|
252
252
|
/** What init touched, so a caller can report it and a test can assert it. */
|
|
253
|
-
type InitTarget = "penv-dir" | "schema" | "config" | "tsconfig" | "gitignore";
|
|
253
|
+
type InitTarget = "penv-dir" | "schema" | "config" | "tsconfig" | "gitignore" | "seam";
|
|
254
254
|
/**
|
|
255
255
|
* `conflicted` is the one that is not a success. penv wanted to write something,
|
|
256
256
|
* found the user's file already saying something else about the same thing, and
|
|
257
257
|
* left it alone — so the step is reported with a warning rather than a ✓, and the
|
|
258
258
|
* text says what will not work until the user decides.
|
|
259
|
+
*
|
|
260
|
+
* `info` is a step penv did not perform automatically — a manual instruction (the
|
|
261
|
+
* injection seam for a framework penv cannot scaffold), reported so the user
|
|
262
|
+
* knows the one thing left to do.
|
|
259
263
|
*/
|
|
260
|
-
type InitAction = "created" | "kept" | "updated" | "conflicted";
|
|
264
|
+
type InitAction = "created" | "kept" | "updated" | "conflicted" | "info";
|
|
261
265
|
interface InitStep {
|
|
262
266
|
readonly target: InitTarget;
|
|
263
267
|
readonly action: InitAction;
|
|
@@ -289,6 +293,13 @@ interface InitDecisions {
|
|
|
289
293
|
* offers that.
|
|
290
294
|
*/
|
|
291
295
|
readonly alias: string;
|
|
296
|
+
/**
|
|
297
|
+
* Whether to inject the validated config into `process.env` for libraries that
|
|
298
|
+
* read it directly, so `env.ts` loads with `{ inject: true }` and penv places
|
|
299
|
+
* the framework's pre-app seam. Off by default and only ever turned on by an
|
|
300
|
+
* explicit yes — a project that reads config only through `@env` gets none.
|
|
301
|
+
*/
|
|
302
|
+
readonly inject: boolean;
|
|
292
303
|
}
|
|
293
304
|
interface InitResult {
|
|
294
305
|
readonly root: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -250,14 +250,18 @@ declare function runGet(options: GetOptions): Promise<string>;
|
|
|
250
250
|
declare function runExplain(options: GetOptions): Promise<GetExplanation>;
|
|
251
251
|
|
|
252
252
|
/** What init touched, so a caller can report it and a test can assert it. */
|
|
253
|
-
type InitTarget = "penv-dir" | "schema" | "config" | "tsconfig" | "gitignore";
|
|
253
|
+
type InitTarget = "penv-dir" | "schema" | "config" | "tsconfig" | "gitignore" | "seam";
|
|
254
254
|
/**
|
|
255
255
|
* `conflicted` is the one that is not a success. penv wanted to write something,
|
|
256
256
|
* found the user's file already saying something else about the same thing, and
|
|
257
257
|
* left it alone — so the step is reported with a warning rather than a ✓, and the
|
|
258
258
|
* text says what will not work until the user decides.
|
|
259
|
+
*
|
|
260
|
+
* `info` is a step penv did not perform automatically — a manual instruction (the
|
|
261
|
+
* injection seam for a framework penv cannot scaffold), reported so the user
|
|
262
|
+
* knows the one thing left to do.
|
|
259
263
|
*/
|
|
260
|
-
type InitAction = "created" | "kept" | "updated" | "conflicted";
|
|
264
|
+
type InitAction = "created" | "kept" | "updated" | "conflicted" | "info";
|
|
261
265
|
interface InitStep {
|
|
262
266
|
readonly target: InitTarget;
|
|
263
267
|
readonly action: InitAction;
|
|
@@ -289,6 +293,13 @@ interface InitDecisions {
|
|
|
289
293
|
* offers that.
|
|
290
294
|
*/
|
|
291
295
|
readonly alias: string;
|
|
296
|
+
/**
|
|
297
|
+
* Whether to inject the validated config into `process.env` for libraries that
|
|
298
|
+
* read it directly, so `env.ts` loads with `{ inject: true }` and penv places
|
|
299
|
+
* the framework's pre-app seam. Off by default and only ever turned on by an
|
|
300
|
+
* explicit yes — a project that reads config only through `@env` gets none.
|
|
301
|
+
*/
|
|
302
|
+
readonly inject: boolean;
|
|
292
303
|
}
|
|
293
304
|
interface InitResult {
|
|
294
305
|
readonly root: string;
|
package/dist/index.js
CHANGED
|
@@ -350,19 +350,19 @@ import {
|
|
|
350
350
|
refFromAccessPath as refFromAccessPath2,
|
|
351
351
|
variableName
|
|
352
352
|
} from "@penvhq/core";
|
|
353
|
-
function defOf(
|
|
354
|
-
if (typeof
|
|
353
|
+
function defOf(node2) {
|
|
354
|
+
if (typeof node2 !== "object" || node2 === null) {
|
|
355
355
|
return void 0;
|
|
356
356
|
}
|
|
357
|
-
const def =
|
|
357
|
+
const def = node2.def;
|
|
358
358
|
return typeof def === "object" && def !== null ? def : void 0;
|
|
359
359
|
}
|
|
360
|
-
function typeOf(
|
|
361
|
-
const type = defOf(
|
|
360
|
+
function typeOf(node2) {
|
|
361
|
+
const type = defOf(node2)?.type;
|
|
362
362
|
return typeof type === "string" ? type : void 0;
|
|
363
363
|
}
|
|
364
|
-
function unwrap(
|
|
365
|
-
let current =
|
|
364
|
+
function unwrap(node2) {
|
|
365
|
+
let current = node2;
|
|
366
366
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
367
367
|
const inner = defOf(current)?.innerType;
|
|
368
368
|
if (inner === void 0) {
|
|
@@ -372,38 +372,38 @@ function unwrap(node) {
|
|
|
372
372
|
}
|
|
373
373
|
return current;
|
|
374
374
|
}
|
|
375
|
-
function shapeOf(
|
|
376
|
-
if (typeOf(
|
|
375
|
+
function shapeOf(node2) {
|
|
376
|
+
if (typeOf(node2) !== "object") {
|
|
377
377
|
return void 0;
|
|
378
378
|
}
|
|
379
|
-
const shape =
|
|
379
|
+
const shape = node2.shape;
|
|
380
380
|
return typeof shape === "object" && shape !== null ? shape : void 0;
|
|
381
381
|
}
|
|
382
382
|
function lookup(root, path) {
|
|
383
|
-
let
|
|
383
|
+
let node2 = unwrap(root);
|
|
384
384
|
for (const key of path) {
|
|
385
|
-
const shape = shapeOf(
|
|
385
|
+
const shape = shapeOf(node2);
|
|
386
386
|
if (shape === void 0) {
|
|
387
387
|
return { kind: "unknown" };
|
|
388
388
|
}
|
|
389
389
|
if (!Object.hasOwn(shape, key)) {
|
|
390
390
|
return { kind: "absent" };
|
|
391
391
|
}
|
|
392
|
-
|
|
392
|
+
node2 = unwrap(shape[key]);
|
|
393
393
|
}
|
|
394
|
-
return { kind: "found", node };
|
|
394
|
+
return { kind: "found", node: node2 };
|
|
395
395
|
}
|
|
396
|
-
function minLengthOf(
|
|
397
|
-
if (typeOf(
|
|
396
|
+
function minLengthOf(node2) {
|
|
397
|
+
if (typeOf(node2) !== "string") {
|
|
398
398
|
return void 0;
|
|
399
399
|
}
|
|
400
|
-
const min =
|
|
400
|
+
const min = node2.minLength;
|
|
401
401
|
return typeof min === "number" ? min : void 0;
|
|
402
402
|
}
|
|
403
403
|
var ABSENCE_PERMITTED = /* @__PURE__ */ new Set(["optional", "default", "catch", "prefault"]);
|
|
404
404
|
var ABSENCE_REFUSED = /* @__PURE__ */ new Set(["nullable", "nonoptional", "readonly"]);
|
|
405
|
-
function permitsAbsence(
|
|
406
|
-
let current =
|
|
405
|
+
function permitsAbsence(node2) {
|
|
406
|
+
let current = node2;
|
|
407
407
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
408
408
|
const type = typeOf(current);
|
|
409
409
|
if (type !== void 0 && ABSENCE_PERMITTED.has(type)) {
|
|
@@ -420,13 +420,13 @@ function permitsAbsence(node) {
|
|
|
420
420
|
}
|
|
421
421
|
return void 0;
|
|
422
422
|
}
|
|
423
|
-
function leaves(
|
|
424
|
-
const own = permitsAbsence(
|
|
423
|
+
function leaves(node2, path, inherited, out2) {
|
|
424
|
+
const own = permitsAbsence(node2);
|
|
425
425
|
const absencePermitted = inherited === true || own === true ? true : combine(inherited, own);
|
|
426
|
-
const shape = shapeOf(unwrap(
|
|
426
|
+
const shape = shapeOf(unwrap(node2));
|
|
427
427
|
if (shape === void 0) {
|
|
428
428
|
if (path.length > 0) {
|
|
429
|
-
out2.push({ path, absencePermitted, node });
|
|
429
|
+
out2.push({ path, absencePermitted, node: node2 });
|
|
430
430
|
}
|
|
431
431
|
return;
|
|
432
432
|
}
|
|
@@ -442,8 +442,8 @@ function declaredLeaves(schema) {
|
|
|
442
442
|
leaves(schema, [], false, out2);
|
|
443
443
|
return out2;
|
|
444
444
|
}
|
|
445
|
-
function defaultValueOf(
|
|
446
|
-
let current =
|
|
445
|
+
function defaultValueOf(node2) {
|
|
446
|
+
let current = node2;
|
|
447
447
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
448
448
|
const def = defOf(current);
|
|
449
449
|
if (def === void 0) {
|
|
@@ -1110,18 +1110,18 @@ function place(root, path, value) {
|
|
|
1110
1110
|
if (leaf === void 0) {
|
|
1111
1111
|
return;
|
|
1112
1112
|
}
|
|
1113
|
-
let
|
|
1113
|
+
let node2 = root;
|
|
1114
1114
|
for (const key of path.slice(0, -1)) {
|
|
1115
|
-
const existing =
|
|
1115
|
+
const existing = node2[key];
|
|
1116
1116
|
if (typeof existing === "object" && existing !== null) {
|
|
1117
|
-
|
|
1117
|
+
node2 = existing;
|
|
1118
1118
|
continue;
|
|
1119
1119
|
}
|
|
1120
1120
|
const child = {};
|
|
1121
|
-
|
|
1122
|
-
|
|
1121
|
+
node2[key] = child;
|
|
1122
|
+
node2 = child;
|
|
1123
1123
|
}
|
|
1124
|
-
|
|
1124
|
+
node2[leaf] = value;
|
|
1125
1125
|
}
|
|
1126
1126
|
function isZodType(value) {
|
|
1127
1127
|
return typeof value === "object" && value !== null && "safeParse" in value && typeof value.safeParse === "function";
|
|
@@ -2805,6 +2805,8 @@ import { join } from "path";
|
|
|
2805
2805
|
import { DEFAULT_SCHEMA_FILE } from "@penvhq/core";
|
|
2806
2806
|
var SIGNATURES = [
|
|
2807
2807
|
{ name: "Next.js", packages: ["next"], publicPrefixes: ["NEXT_PUBLIC_"] },
|
|
2808
|
+
{ name: "SvelteKit", packages: ["@sveltejs/kit"], publicPrefixes: ["PUBLIC_"] },
|
|
2809
|
+
{ name: "Nuxt", packages: ["nuxt"], publicPrefixes: ["NUXT_PUBLIC_"] },
|
|
2808
2810
|
{
|
|
2809
2811
|
name: "TanStack Start",
|
|
2810
2812
|
packages: ["@tanstack/react-start", "@tanstack/start"],
|
|
@@ -2878,17 +2880,23 @@ function detectFramework(cwd) {
|
|
|
2878
2880
|
}
|
|
2879
2881
|
for (const signature of SIGNATURES) {
|
|
2880
2882
|
if (signature.packages.some((name) => dependencies.has(name))) {
|
|
2881
|
-
|
|
2882
|
-
return {
|
|
2883
|
-
name: signature.name,
|
|
2884
|
-
schemaFile: schema.file,
|
|
2885
|
-
...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
|
|
2886
|
-
publicPrefixes: signature.publicPrefixes
|
|
2887
|
-
};
|
|
2883
|
+
return detectedFrom(cwd, signature.name, signature.publicPrefixes);
|
|
2888
2884
|
}
|
|
2889
2885
|
}
|
|
2886
|
+
if (existsSync(join(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
|
|
2887
|
+
return detectedFrom(cwd, "Bun", []);
|
|
2888
|
+
}
|
|
2890
2889
|
return void 0;
|
|
2891
2890
|
}
|
|
2891
|
+
function detectedFrom(cwd, name, publicPrefixes) {
|
|
2892
|
+
const schema = schemaFileFor(cwd);
|
|
2893
|
+
return {
|
|
2894
|
+
name,
|
|
2895
|
+
schemaFile: schema.file,
|
|
2896
|
+
...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
|
|
2897
|
+
publicPrefixes
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2892
2900
|
var DEFAULT_ALIAS = "@env";
|
|
2893
2901
|
var IMPORTS_ALIAS = "#env";
|
|
2894
2902
|
function detectAlias(cwd) {
|
|
@@ -2915,6 +2923,132 @@ import {
|
|
|
2915
2923
|
validateSchemaFile
|
|
2916
2924
|
} from "@penvhq/core";
|
|
2917
2925
|
import { defineCommand as defineCommand9 } from "citty";
|
|
2926
|
+
|
|
2927
|
+
// src/seams.ts
|
|
2928
|
+
function nextjs({ alias, srcDir }) {
|
|
2929
|
+
return {
|
|
2930
|
+
kind: "scaffold",
|
|
2931
|
+
file: `${srcDir}instrumentation.ts`,
|
|
2932
|
+
content: `// This is instrumentation.ts, Next.js's own startup hook \u2014 Next runs it once,
|
|
2933
|
+
// before any of your app code, on every server boot. penv put one line here so
|
|
2934
|
+
// your config is in process.env before the first library reads it. The filename
|
|
2935
|
+
// is Next's requirement (it only finds \`instrumentation.ts\`), so keep it as-is.
|
|
2936
|
+
export async function register() {
|
|
2937
|
+
// Next also calls register on the Edge runtime, where penv cannot read the
|
|
2938
|
+
// filesystem \u2014 so inject runs on the Node runtime only.
|
|
2939
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
2940
|
+
await import("${alias}");
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
`,
|
|
2944
|
+
ifPresent: `Add to your existing \`${srcDir}instrumentation.ts\`, inside \`register()\` and first:
|
|
2945
|
+
if (process.env.NEXT_RUNTIME === "nodejs") { await import("${alias}"); }`,
|
|
2946
|
+
notes: []
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
function sveltekit({ alias }) {
|
|
2950
|
+
return {
|
|
2951
|
+
kind: "scaffold",
|
|
2952
|
+
// SvelteKit's hooks module is always under `src/`, whatever the schema path.
|
|
2953
|
+
file: "src/hooks.server.ts",
|
|
2954
|
+
content: `// This is hooks.server.ts, SvelteKit's server-startup module \u2014 it loads before
|
|
2955
|
+
// your routes handle a request. penv put this import first so your config is in
|
|
2956
|
+
// process.env before any library reads it. Keep it the first line; the name and
|
|
2957
|
+
// location are SvelteKit's, not penv's.
|
|
2958
|
+
import "${alias}";
|
|
2959
|
+
`,
|
|
2960
|
+
ifPresent: `Add \`import "${alias}";\` as the FIRST line of src/hooks.server.ts, above every other import.`,
|
|
2961
|
+
notes: [
|
|
2962
|
+
`SvelteKit resolves app aliases through \`kit.alias\` \u2014 make sure \`${alias}\` is registered in svelte.config.js, not only tsconfig.`
|
|
2963
|
+
]
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
function nuxt({ alias }) {
|
|
2967
|
+
return {
|
|
2968
|
+
kind: "scaffold",
|
|
2969
|
+
// The `0.` prefix is load-bearing: Nitro sorts plugin filenames as strings,
|
|
2970
|
+
// so it must sort first to run before any other plugin reads process.env.
|
|
2971
|
+
file: "server/plugins/0.penv.ts",
|
|
2972
|
+
content: `// This is a Nitro server plugin (Nuxt's server layer runs it once at startup,
|
|
2973
|
+
// before any middleware or request handler). penv put the import here so your
|
|
2974
|
+
// config is in process.env before a library reads it. The \`0.\` prefix keeps this
|
|
2975
|
+
// plugin first \u2014 Nitro sorts plugin filenames as text \u2014 so don't rename it.
|
|
2976
|
+
import "${alias}";
|
|
2977
|
+
|
|
2978
|
+
// Nitro requires every plugin file to default-export a function; the injection
|
|
2979
|
+
// above already ran at import, so this body is intentionally empty.
|
|
2980
|
+
export default defineNitroPlugin(() => {});
|
|
2981
|
+
`,
|
|
2982
|
+
ifPresent: `Create server/plugins/0.penv.ts with \`import "${alias}";\` and \`export default defineNitroPlugin(() => {});\`.`,
|
|
2983
|
+
notes: [
|
|
2984
|
+
`Wire \`${alias}\` for the Nitro (server) build so it never leaks into the client bundle.`
|
|
2985
|
+
]
|
|
2986
|
+
};
|
|
2987
|
+
}
|
|
2988
|
+
function bun({ alias }) {
|
|
2989
|
+
return {
|
|
2990
|
+
kind: "scaffold",
|
|
2991
|
+
file: ".penv/preload.ts",
|
|
2992
|
+
content: `// Bun evaluates this file before your entry point, because it is listed under
|
|
2993
|
+
// \`preload\` in bunfig.toml (see the note penv printed). Running it injects your
|
|
2994
|
+
// config into process.env before any library reads it.
|
|
2995
|
+
import "${alias}";
|
|
2996
|
+
`,
|
|
2997
|
+
ifPresent: `Keep .penv/preload.ts as \`import "${alias}";\`.`,
|
|
2998
|
+
notes: [
|
|
2999
|
+
'Register it in bunfig.toml so Bun runs it: `preload = ["./.penv/preload.ts"]` (and the same under `[test]`).',
|
|
3000
|
+
`\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`
|
|
3001
|
+
]
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
function node({ alias, srcDir }) {
|
|
3005
|
+
const runtimeSpecifier = alias.startsWith("#") ? alias : `./${srcDir}.penv/env.js`;
|
|
3006
|
+
return {
|
|
3007
|
+
kind: "instruct",
|
|
3008
|
+
instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
|
|
3009
|
+
code, so place the injection yourself, one of two ways (both run first):
|
|
3010
|
+
\u2022 Preload at launch \u2014 add to your start script: node --import "${runtimeSpecifier}" your-entry.js
|
|
3011
|
+
\u2022 Or make it the FIRST line of your entry file: import "${runtimeSpecifier}";
|
|
3012
|
+
` + (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.
|
|
3013
|
+
`)
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
function tanstack({ alias, srcDir }) {
|
|
3017
|
+
return {
|
|
3018
|
+
kind: "instruct",
|
|
3019
|
+
instruction: `Add \`import "${alias}";\` as the FIRST line of ${srcDir}server.ts (TanStack Start's
|
|
3020
|
+
server entry), above the \`@tanstack/react-start/server-entry\` import \u2014 nothing may
|
|
3021
|
+
import before it. If you have no server.ts yet, create it from TanStack's server-entry
|
|
3022
|
+
template and put that import first.`
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
function astro(_context) {
|
|
3026
|
+
return {
|
|
3027
|
+
kind: "instruct",
|
|
3028
|
+
instruction: 'Astro has no single pre-app hook. Add a small integration to astro.config that\ninjects the import into every SSR page, then cover the routes it cannot reach:\n \u2022 In astro.config integrations, add:\n { name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>\n injectScript("page-ssr", \'import "@env";\') } }\n \u2022 Also add `import "@env";` as the first line of src/middleware.ts and of any\n endpoint route (src/pages/*.ts) that imports a library reading process.env.\nInjection only applies under a server (on-demand) adapter, not a static build.'
|
|
3029
|
+
};
|
|
3030
|
+
}
|
|
3031
|
+
function viteSpa(_context) {
|
|
3032
|
+
return {
|
|
3033
|
+
kind: "none",
|
|
3034
|
+
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."
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
var SEAMS = {
|
|
3038
|
+
"Next.js": nextjs,
|
|
3039
|
+
SvelteKit: sveltekit,
|
|
3040
|
+
Nuxt: nuxt,
|
|
3041
|
+
Bun: bun,
|
|
3042
|
+
"TanStack Start": tanstack,
|
|
3043
|
+
Astro: astro,
|
|
3044
|
+
Vite: viteSpa
|
|
3045
|
+
};
|
|
3046
|
+
function seamFor(framework, context) {
|
|
3047
|
+
const build = framework === void 0 ? node : SEAMS[framework] ?? node;
|
|
3048
|
+
return build(context);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// src/commands/init.ts
|
|
2918
3052
|
var CONFIG_FILE = "penv.config.ts";
|
|
2919
3053
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
2920
3054
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -2924,6 +3058,7 @@ var IMPORTS_PREFIX = "#";
|
|
|
2924
3058
|
var PACKAGE_FILE = "package.json";
|
|
2925
3059
|
var DEFAULT_DECISIONS = {
|
|
2926
3060
|
environments: [],
|
|
3061
|
+
inject: false,
|
|
2927
3062
|
schemaFile: DEFAULT_SCHEMA_FILE2,
|
|
2928
3063
|
publicPrefixes: [],
|
|
2929
3064
|
alias: DEFAULT_ALIAS
|
|
@@ -3042,6 +3177,9 @@ function planInit(root, flags = {}) {
|
|
|
3042
3177
|
detected,
|
|
3043
3178
|
decisions: {
|
|
3044
3179
|
environments,
|
|
3180
|
+
// Injection is a choice about the app's needs, not something on disk — so
|
|
3181
|
+
// the plan defaults it off, and only the interactive prompt turns it on.
|
|
3182
|
+
inject: false,
|
|
3045
3183
|
schemaFile,
|
|
3046
3184
|
publicPrefixes: declared?.publicPrefixes ?? detected?.publicPrefixes ?? [],
|
|
3047
3185
|
alias
|
|
@@ -3088,16 +3226,33 @@ async function promptForDecisions(plan2, io) {
|
|
|
3088
3226
|
);
|
|
3089
3227
|
io.write("");
|
|
3090
3228
|
}
|
|
3229
|
+
const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
|
|
3091
3230
|
const proceed = (await io.ask(prompt("Proceed?", "Y/n"))).trim().toLowerCase();
|
|
3092
3231
|
if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
|
|
3093
3232
|
return void 0;
|
|
3094
3233
|
}
|
|
3095
|
-
return { ...plan2.decisions, environments };
|
|
3234
|
+
return { ...plan2.decisions, environments, inject };
|
|
3235
|
+
}
|
|
3236
|
+
function seamKindFor(plan2) {
|
|
3237
|
+
return seamFor(plan2.detected?.name, {
|
|
3238
|
+
alias: plan2.decisions.alias,
|
|
3239
|
+
srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : ""
|
|
3240
|
+
}).kind;
|
|
3241
|
+
}
|
|
3242
|
+
async function askInject(io) {
|
|
3243
|
+
io.write("");
|
|
3244
|
+
io.write("Also inject your validated config into process.env?");
|
|
3245
|
+
io.write(" y libraries that read process.env directly (WorkOS, Prisma\u2026) just work");
|
|
3246
|
+
io.write(' n config stays available only through import { env } from "@env"');
|
|
3247
|
+
const answer = (await io.ask(prompt("inject", "y/N"))).trim().toLowerCase();
|
|
3248
|
+
return answer === "y" || answer === "yes";
|
|
3096
3249
|
}
|
|
3097
3250
|
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.";
|
|
3098
3251
|
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";
|
|
3099
|
-
function renderSchemaModule(fields, draft) {
|
|
3252
|
+
function renderSchemaModule(fields, draft, inject = false) {
|
|
3100
3253
|
const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
|
|
3254
|
+
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";
|
|
3255
|
+
const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
|
|
3101
3256
|
return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
|
|
3102
3257
|
import { load } from "@penvhq/penv";
|
|
3103
3258
|
|
|
@@ -3107,11 +3262,7 @@ export const schema = z.object({
|
|
|
3107
3262
|
${body}
|
|
3108
3263
|
});
|
|
3109
3264
|
|
|
3110
|
-
|
|
3111
|
-
// code. Importing it loads configuration and throws (naming the parameter and
|
|
3112
|
-
// environment) if anything required is missing or invalid.
|
|
3113
|
-
export const env = load(schema);
|
|
3114
|
-
|
|
3265
|
+
` + loadComment + loadCall + `
|
|
3115
3266
|
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3116
3267
|
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3117
3268
|
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
@@ -3403,7 +3554,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
3403
3554
|
};
|
|
3404
3555
|
}
|
|
3405
3556
|
mkdirSync(dirname3(file), { recursive: true });
|
|
3406
|
-
writeFileSync2(file, renderSchemaModule(fields, draft), "utf8");
|
|
3557
|
+
writeFileSync2(file, renderSchemaModule(fields, draft, decisions.inject), "utf8");
|
|
3407
3558
|
return {
|
|
3408
3559
|
target: "schema",
|
|
3409
3560
|
action: "created",
|
|
@@ -3480,14 +3631,56 @@ function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3480
3631
|
text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
|
|
3481
3632
|
};
|
|
3482
3633
|
}
|
|
3634
|
+
function srcDirOf(root) {
|
|
3635
|
+
return existsSync2(join2(root, "src")) ? "src/" : "";
|
|
3636
|
+
}
|
|
3637
|
+
function writeSeam(root, decisions = DEFAULT_DECISIONS) {
|
|
3638
|
+
if (!decisions.inject) {
|
|
3639
|
+
return void 0;
|
|
3640
|
+
}
|
|
3641
|
+
const framework = detectFramework(root)?.name;
|
|
3642
|
+
const seam = seamFor(framework, { alias: decisions.alias, srcDir: srcDirOf(root) });
|
|
3643
|
+
if (seam.kind === "none") {
|
|
3644
|
+
return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
|
|
3645
|
+
}
|
|
3646
|
+
if (seam.kind === "instruct") {
|
|
3647
|
+
return {
|
|
3648
|
+
target: "seam",
|
|
3649
|
+
action: "info",
|
|
3650
|
+
text: "Place the injection seam",
|
|
3651
|
+
note: seam.instruction
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
const file = join2(root, ...seam.file.split("/"));
|
|
3655
|
+
const notes = seam.notes.length === 0 ? "" : `
|
|
3656
|
+
${seam.notes.map((n) => ` ${n}`).join("\n")}`;
|
|
3657
|
+
if (existsSync2(file)) {
|
|
3658
|
+
return {
|
|
3659
|
+
target: "seam",
|
|
3660
|
+
action: "info",
|
|
3661
|
+
text: `Add the injection seam to ${seam.file}`,
|
|
3662
|
+
note: `${seam.ifPresent}${notes}`
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3665
|
+
mkdirSync(dirname3(file), { recursive: true });
|
|
3666
|
+
writeFileSync2(file, seam.content, "utf8");
|
|
3667
|
+
return {
|
|
3668
|
+
target: "seam",
|
|
3669
|
+
action: "created",
|
|
3670
|
+
text: `Wrote ${seam.file} (runs the injection before your app)`,
|
|
3671
|
+
...notes === "" ? {} : { note: notes.trimStart() }
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3483
3674
|
function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
3484
|
-
|
|
3675
|
+
const steps = [
|
|
3485
3676
|
ensurePenvDir(root),
|
|
3486
3677
|
writeSchemaFile(root, fields, draft, decisions),
|
|
3487
3678
|
writeConfigFile(root, decisions),
|
|
3488
3679
|
writeTsconfigAlias(root, decisions),
|
|
3489
3680
|
writeGitignore(root, decisions)
|
|
3490
3681
|
];
|
|
3682
|
+
const seam = writeSeam(root, decisions);
|
|
3683
|
+
return seam === void 0 ? steps : [...steps, seam];
|
|
3491
3684
|
}
|
|
3492
3685
|
function runInit(options) {
|
|
3493
3686
|
const root = resolve4(options.cwd);
|
|
@@ -3496,7 +3689,7 @@ function runInit(options) {
|
|
|
3496
3689
|
}
|
|
3497
3690
|
function renderInit(result2) {
|
|
3498
3691
|
const steps = result2.steps.map((step) => {
|
|
3499
|
-
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
3692
|
+
const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
|
|
3500
3693
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
3501
3694
|
});
|
|
3502
3695
|
return [
|
|
@@ -3729,6 +3922,9 @@ function environmentNamed(file, explicit) {
|
|
|
3729
3922
|
function decisionsOf(config, cwd) {
|
|
3730
3923
|
return {
|
|
3731
3924
|
environments: config.environments,
|
|
3925
|
+
// `import` re-scaffolds env.ts for an existing project; injection is an init
|
|
3926
|
+
// choice, so it is not turned on here.
|
|
3927
|
+
inject: false,
|
|
3732
3928
|
schemaFile: schemaFileOf3(config),
|
|
3733
3929
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
3734
3930
|
alias: detectAlias(cwd)
|