@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.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;
|
|
@@ -299,6 +310,8 @@ interface InitOptions {
|
|
|
299
310
|
readonly cwd: string;
|
|
300
311
|
/** What to write. Omitted means the plan's defaults, as `--yes` takes them. */
|
|
301
312
|
readonly decisions?: InitDecisions;
|
|
313
|
+
/** The detected framework name, passed by the command so the seam step need not re-detect it. */
|
|
314
|
+
readonly framework?: string;
|
|
302
315
|
}
|
|
303
316
|
interface AliasEdit {
|
|
304
317
|
readonly source: 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;
|
|
@@ -299,6 +310,8 @@ interface InitOptions {
|
|
|
299
310
|
readonly cwd: string;
|
|
300
311
|
/** What to write. Omitted means the plan's defaults, as `--yes` takes them. */
|
|
301
312
|
readonly decisions?: InitDecisions;
|
|
313
|
+
/** The detected framework name, passed by the command so the seam step need not re-detect it. */
|
|
314
|
+
readonly framework?: string;
|
|
302
315
|
}
|
|
303
316
|
interface AliasEdit {
|
|
304
317
|
readonly source: 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"],
|
|
@@ -2831,7 +2833,7 @@ function occupied(cwd, relative3) {
|
|
|
2831
2833
|
return existsSync(file) && !exportsSchema(file);
|
|
2832
2834
|
}
|
|
2833
2835
|
function schemaFileFor(cwd) {
|
|
2834
|
-
const dir =
|
|
2836
|
+
const dir = srcPrefix(cwd);
|
|
2835
2837
|
const preferred = `${dir}env.ts`;
|
|
2836
2838
|
if (!occupied(cwd, preferred)) {
|
|
2837
2839
|
return { file: preferred };
|
|
@@ -2842,6 +2844,9 @@ function schemaFileFor(cwd) {
|
|
|
2842
2844
|
}
|
|
2843
2845
|
return { file: DEFAULT_SCHEMA_FILE, displaced: preferred };
|
|
2844
2846
|
}
|
|
2847
|
+
function srcPrefix(cwd) {
|
|
2848
|
+
return existsSync(join(cwd, "src")) ? "src/" : "";
|
|
2849
|
+
}
|
|
2845
2850
|
function dependenciesOf(cwd) {
|
|
2846
2851
|
const manifest = manifestOf(cwd);
|
|
2847
2852
|
if (manifest === void 0) {
|
|
@@ -2878,17 +2883,23 @@ function detectFramework(cwd) {
|
|
|
2878
2883
|
}
|
|
2879
2884
|
for (const signature of SIGNATURES) {
|
|
2880
2885
|
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
|
-
};
|
|
2886
|
+
return detectedFrom(cwd, signature.name, signature.publicPrefixes);
|
|
2888
2887
|
}
|
|
2889
2888
|
}
|
|
2889
|
+
if (existsSync(join(cwd, "bunfig.toml")) || dependencies.has("bun-types")) {
|
|
2890
|
+
return detectedFrom(cwd, "Bun", []);
|
|
2891
|
+
}
|
|
2890
2892
|
return void 0;
|
|
2891
2893
|
}
|
|
2894
|
+
function detectedFrom(cwd, name, publicPrefixes) {
|
|
2895
|
+
const schema = schemaFileFor(cwd);
|
|
2896
|
+
return {
|
|
2897
|
+
name,
|
|
2898
|
+
schemaFile: schema.file,
|
|
2899
|
+
...schema.displaced === void 0 ? {} : { displacedFrom: schema.displaced },
|
|
2900
|
+
publicPrefixes
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2892
2903
|
var DEFAULT_ALIAS = "@env";
|
|
2893
2904
|
var IMPORTS_ALIAS = "#env";
|
|
2894
2905
|
function detectAlias(cwd) {
|
|
@@ -2915,6 +2926,145 @@ import {
|
|
|
2915
2926
|
validateSchemaFile
|
|
2916
2927
|
} from "@penvhq/core";
|
|
2917
2928
|
import { defineCommand as defineCommand9 } from "citty";
|
|
2929
|
+
|
|
2930
|
+
// src/seams.ts
|
|
2931
|
+
function nextjs({ alias, srcDir }) {
|
|
2932
|
+
return {
|
|
2933
|
+
kind: "scaffold",
|
|
2934
|
+
file: `${srcDir}instrumentation.ts`,
|
|
2935
|
+
content: `// This is instrumentation.ts, Next.js's own startup hook \u2014 Next runs it once,
|
|
2936
|
+
// before any of your app code, on every server boot. penv put one line here so
|
|
2937
|
+
// your config is in process.env before the first library reads it. The filename
|
|
2938
|
+
// is Next's requirement (it only finds \`instrumentation.ts\`), so keep it as-is.
|
|
2939
|
+
export async function register() {
|
|
2940
|
+
// Next also calls register on the Edge runtime, where penv cannot read the
|
|
2941
|
+
// filesystem \u2014 so inject runs on the Node runtime only.
|
|
2942
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
2943
|
+
await import("${alias}");
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
`,
|
|
2947
|
+
ifPresent: `Add to your existing \`${srcDir}instrumentation.ts\`, inside \`register()\` and first:
|
|
2948
|
+
if (process.env.NEXT_RUNTIME === "nodejs") { await import("${alias}"); }`,
|
|
2949
|
+
notes: []
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
function sveltekit({ alias }) {
|
|
2953
|
+
return {
|
|
2954
|
+
kind: "scaffold",
|
|
2955
|
+
// SvelteKit's hooks module is always under `src/`, whatever the schema path.
|
|
2956
|
+
file: "src/hooks.server.ts",
|
|
2957
|
+
content: `// This is hooks.server.ts, SvelteKit's server-startup module \u2014 it loads before
|
|
2958
|
+
// your routes handle a request. penv put this import first so your config is in
|
|
2959
|
+
// process.env before any library reads it. Keep it the first line; the name and
|
|
2960
|
+
// location are SvelteKit's, not penv's.
|
|
2961
|
+
import "${alias}";
|
|
2962
|
+
`,
|
|
2963
|
+
ifPresent: `Add \`import "${alias}";\` as the FIRST line of src/hooks.server.ts, above every other import.`,
|
|
2964
|
+
notes: [
|
|
2965
|
+
`SvelteKit resolves app aliases through \`kit.alias\` \u2014 make sure \`${alias}\` is registered in svelte.config.js, not only tsconfig.`
|
|
2966
|
+
]
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
function nuxt({ alias }) {
|
|
2970
|
+
return {
|
|
2971
|
+
kind: "scaffold",
|
|
2972
|
+
// The `0.` prefix is load-bearing: Nitro sorts plugin filenames as strings,
|
|
2973
|
+
// so it must sort first to run before any other plugin reads process.env.
|
|
2974
|
+
file: "server/plugins/0.penv.ts",
|
|
2975
|
+
content: `// This is a Nitro server plugin (Nuxt's server layer runs it once at startup,
|
|
2976
|
+
// before any middleware or request handler). penv put the import here so your
|
|
2977
|
+
// config is in process.env before a library reads it. The \`0.\` prefix keeps this
|
|
2978
|
+
// plugin first \u2014 Nitro sorts plugin filenames as text \u2014 so don't rename it.
|
|
2979
|
+
import "${alias}";
|
|
2980
|
+
|
|
2981
|
+
// Nitro requires every plugin file to default-export a function; the injection
|
|
2982
|
+
// above already ran at import, so this body is intentionally empty.
|
|
2983
|
+
export default defineNitroPlugin(() => {});
|
|
2984
|
+
`,
|
|
2985
|
+
ifPresent: `Create server/plugins/0.penv.ts with \`import "${alias}";\` and \`export default defineNitroPlugin(() => {});\`.`,
|
|
2986
|
+
notes: [
|
|
2987
|
+
`Wire \`${alias}\` for the Nitro (server) build so it never leaks into the client bundle.`
|
|
2988
|
+
]
|
|
2989
|
+
};
|
|
2990
|
+
}
|
|
2991
|
+
function bun({ alias }) {
|
|
2992
|
+
return {
|
|
2993
|
+
kind: "scaffold",
|
|
2994
|
+
file: ".penv/preload.ts",
|
|
2995
|
+
content: `// Bun evaluates this file before your entry point, because it is listed under
|
|
2996
|
+
// \`preload\` in bunfig.toml. Running it injects your config into process.env
|
|
2997
|
+
// before any library reads it.
|
|
2998
|
+
import "${alias}";
|
|
2999
|
+
`,
|
|
3000
|
+
ifPresent: `Keep .penv/preload.ts as \`import "${alias}";\`.`,
|
|
3001
|
+
notes: [`\`${alias}\` must be a tsconfig \`paths\` alias for Bun to resolve it.`],
|
|
3002
|
+
// The preload file is inert until bunfig.toml points at it — so penv writes
|
|
3003
|
+
// that too, both for the runtime and for `bun test`. Only when there is no
|
|
3004
|
+
// bunfig to overwrite; an existing one is the user's, and penv prints instead.
|
|
3005
|
+
also: {
|
|
3006
|
+
file: "bunfig.toml",
|
|
3007
|
+
content: 'preload = ["./.penv/preload.ts"]\n\n[test]\npreload = ["./.penv/preload.ts"]\n',
|
|
3008
|
+
ifPresent: 'Add "./.penv/preload.ts" to the `preload` array in bunfig.toml \u2014 and mirror it under `[test]` so `bun test` injects too.'
|
|
3009
|
+
}
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
function node({ alias, schemaFile }) {
|
|
3013
|
+
const builtSchema = `./${schemaFile.replace(/\.ts$/, ".js")}`;
|
|
3014
|
+
const runtimeSpecifier = alias.startsWith("#") ? alias : builtSchema;
|
|
3015
|
+
return {
|
|
3016
|
+
kind: "instruct",
|
|
3017
|
+
instruction: `Plain Node / Express / Fastify have no framework hook that runs before your
|
|
3018
|
+
code, so place the injection yourself, one of two ways (both run first):
|
|
3019
|
+
\u2022 Preload at launch \u2014 add to your start script: node --import "${runtimeSpecifier}" your-entry.js
|
|
3020
|
+
\u2022 Or make it the FIRST line of your entry file: import "${runtimeSpecifier}";
|
|
3021
|
+
` + (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.
|
|
3022
|
+
`)
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
function tanstack({ alias, srcDir }) {
|
|
3026
|
+
return {
|
|
3027
|
+
kind: "instruct",
|
|
3028
|
+
instruction: `Add \`import "${alias}";\` as the FIRST line of ${srcDir}server.ts (TanStack Start's
|
|
3029
|
+
server entry), above the \`@tanstack/react-start/server-entry\` import \u2014 nothing may
|
|
3030
|
+
import before it. If you have no server.ts yet, create it from TanStack's server-entry
|
|
3031
|
+
template and put that import first.`
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
function astro({ alias }) {
|
|
3035
|
+
return {
|
|
3036
|
+
kind: "instruct",
|
|
3037
|
+
instruction: `Astro has no single pre-app hook. Add a small integration to astro.config that
|
|
3038
|
+
injects the import into every SSR page, then cover the routes it cannot reach:
|
|
3039
|
+
\u2022 In astro.config integrations, add:
|
|
3040
|
+
{ name: "penv", hooks: { "astro:config:setup": ({ injectScript }) =>
|
|
3041
|
+
injectScript("page-ssr", 'import "${alias}";') } }
|
|
3042
|
+
\u2022 Also add \`import "${alias}";\` as the first line of src/middleware.ts and of any
|
|
3043
|
+
endpoint route (src/pages/*.ts) that imports a library reading process.env.
|
|
3044
|
+
Injection only applies under a server (on-demand) adapter, not a static build.`
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
function viteSpa(_context) {
|
|
3048
|
+
return {
|
|
3049
|
+
kind: "none",
|
|
3050
|
+
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."
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
var SEAMS = {
|
|
3054
|
+
"Next.js": nextjs,
|
|
3055
|
+
SvelteKit: sveltekit,
|
|
3056
|
+
Nuxt: nuxt,
|
|
3057
|
+
Bun: bun,
|
|
3058
|
+
"TanStack Start": tanstack,
|
|
3059
|
+
Astro: astro,
|
|
3060
|
+
Vite: viteSpa
|
|
3061
|
+
};
|
|
3062
|
+
function seamFor(framework, context) {
|
|
3063
|
+
const build = framework === void 0 ? node : SEAMS[framework] ?? node;
|
|
3064
|
+
return build(context);
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// src/commands/init.ts
|
|
2918
3068
|
var CONFIG_FILE = "penv.config.ts";
|
|
2919
3069
|
var TSCONFIG_FILE = "tsconfig.json";
|
|
2920
3070
|
var GITIGNORE_FILE = ".gitignore";
|
|
@@ -2924,6 +3074,7 @@ var IMPORTS_PREFIX = "#";
|
|
|
2924
3074
|
var PACKAGE_FILE = "package.json";
|
|
2925
3075
|
var DEFAULT_DECISIONS = {
|
|
2926
3076
|
environments: [],
|
|
3077
|
+
inject: false,
|
|
2927
3078
|
schemaFile: DEFAULT_SCHEMA_FILE2,
|
|
2928
3079
|
publicPrefixes: [],
|
|
2929
3080
|
alias: DEFAULT_ALIAS
|
|
@@ -3042,6 +3193,9 @@ function planInit(root, flags = {}) {
|
|
|
3042
3193
|
detected,
|
|
3043
3194
|
decisions: {
|
|
3044
3195
|
environments,
|
|
3196
|
+
// Injection is a choice about the app's needs, not something on disk — so
|
|
3197
|
+
// the plan defaults it off, and only the interactive prompt turns it on.
|
|
3198
|
+
inject: false,
|
|
3045
3199
|
schemaFile,
|
|
3046
3200
|
publicPrefixes: declared?.publicPrefixes ?? detected?.publicPrefixes ?? [],
|
|
3047
3201
|
alias
|
|
@@ -3092,12 +3246,30 @@ async function promptForDecisions(plan2, io) {
|
|
|
3092
3246
|
if (proceed.length > 0 && proceed !== "y" && proceed !== "yes") {
|
|
3093
3247
|
return void 0;
|
|
3094
3248
|
}
|
|
3095
|
-
|
|
3249
|
+
const inject = seamKindFor(plan2) === "none" ? false : await askInject(io);
|
|
3250
|
+
return { ...plan2.decisions, environments, inject };
|
|
3251
|
+
}
|
|
3252
|
+
function seamKindFor(plan2) {
|
|
3253
|
+
return seamFor(plan2.detected?.name, {
|
|
3254
|
+
alias: plan2.decisions.alias,
|
|
3255
|
+
srcDir: plan2.decisions.schemaFile.startsWith("src/") ? "src/" : "",
|
|
3256
|
+
schemaFile: plan2.decisions.schemaFile
|
|
3257
|
+
}).kind;
|
|
3258
|
+
}
|
|
3259
|
+
async function askInject(io) {
|
|
3260
|
+
io.write("");
|
|
3261
|
+
io.write("Also inject your validated config into process.env?");
|
|
3262
|
+
io.write(" y libraries that read process.env directly (WorkOS, Prisma\u2026) just work");
|
|
3263
|
+
io.write(' n config stays available only through import { env } from "@env"');
|
|
3264
|
+
const answer = (await io.ask(prompt("inject", "y/N"))).trim().toLowerCase();
|
|
3265
|
+
return answer === "y" || answer === "yes";
|
|
3096
3266
|
}
|
|
3097
3267
|
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
3268
|
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) {
|
|
3269
|
+
function renderSchemaModule(fields, draft, inject = false) {
|
|
3100
3270
|
const body = fields.length === 0 ? EMPTY_SCHEMA_BODY : fields.map((field) => ` ${field.key}: ${field.type},`).join("\n");
|
|
3271
|
+
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";
|
|
3272
|
+
const loadCall = inject ? "export const env = load(schema, { inject: true });\n" : "export const env = load(schema);\n";
|
|
3101
3273
|
return `${draft ? DRAFT_HEADER : ""}import { z } from "zod";
|
|
3102
3274
|
import { load } from "@penvhq/penv";
|
|
3103
3275
|
|
|
@@ -3107,11 +3279,7 @@ export const schema = z.object({
|
|
|
3107
3279
|
${body}
|
|
3108
3280
|
});
|
|
3109
3281
|
|
|
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
|
-
|
|
3282
|
+
` + loadComment + loadCall + `
|
|
3115
3283
|
// Registers the schema's shape with penv's types (erased at runtime, so
|
|
3116
3284
|
// nothing cycles). This is what makes \`override\` keys in penv.config.ts
|
|
3117
3285
|
// autocomplete from this schema \u2014 a typo'd parameter id is a compile error.
|
|
@@ -3403,7 +3571,7 @@ function writeSchemaFile(root, fields, draft, decisions = DEFAULT_DECISIONS) {
|
|
|
3403
3571
|
};
|
|
3404
3572
|
}
|
|
3405
3573
|
mkdirSync(dirname3(file), { recursive: true });
|
|
3406
|
-
writeFileSync2(file, renderSchemaModule(fields, draft), "utf8");
|
|
3574
|
+
writeFileSync2(file, renderSchemaModule(fields, draft, decisions.inject), "utf8");
|
|
3407
3575
|
return {
|
|
3408
3576
|
target: "schema",
|
|
3409
3577
|
action: "created",
|
|
@@ -3480,23 +3648,119 @@ function writeGitignore(root, decisions = DEFAULT_DECISIONS) {
|
|
|
3480
3648
|
text: `${existing === void 0 ? "Created" : "Updated"} ${relative3}`
|
|
3481
3649
|
};
|
|
3482
3650
|
}
|
|
3483
|
-
|
|
3484
|
-
|
|
3651
|
+
var INJECT_MIN_VERSION = "0.6.0";
|
|
3652
|
+
function outdatedRuntimeWarning(root) {
|
|
3653
|
+
const version = installedPenvVersion(root);
|
|
3654
|
+
if (version === void 0 || !isBelow(version, INJECT_MIN_VERSION)) {
|
|
3655
|
+
return void 0;
|
|
3656
|
+
}
|
|
3657
|
+
return `Injection needs @penvhq/penv ${INJECT_MIN_VERSION}+ \u2014 this project has ${version}, whose \`load\` ignores \`{ inject: true }\`. Upgrade, or process.env stays empty.`;
|
|
3658
|
+
}
|
|
3659
|
+
function installedPenvVersion(root) {
|
|
3660
|
+
const file = join2(root, "node_modules", "@penvhq", "penv", "package.json");
|
|
3661
|
+
if (!existsSync2(file)) {
|
|
3662
|
+
return void 0;
|
|
3663
|
+
}
|
|
3664
|
+
try {
|
|
3665
|
+
const version = JSON.parse(readFileSync2(file, "utf8")).version;
|
|
3666
|
+
return typeof version === "string" ? version : void 0;
|
|
3667
|
+
} catch {
|
|
3668
|
+
return void 0;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
function releaseTriple(version) {
|
|
3672
|
+
const parts = (version.split("-")[0] ?? "").split(".");
|
|
3673
|
+
const at = (i) => Number.parseInt(parts[i] ?? "0", 10) || 0;
|
|
3674
|
+
return [at(0), at(1), at(2)];
|
|
3675
|
+
}
|
|
3676
|
+
function isBelow(a, b) {
|
|
3677
|
+
const [am, an, ap] = releaseTriple(a);
|
|
3678
|
+
const [bm, bn, bp] = releaseTriple(b);
|
|
3679
|
+
if (am !== bm) return am < bm;
|
|
3680
|
+
if (an !== bn) return an < bn;
|
|
3681
|
+
return ap < bp;
|
|
3682
|
+
}
|
|
3683
|
+
function writeSeam(root, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
|
|
3684
|
+
if (!decisions.inject) {
|
|
3685
|
+
return void 0;
|
|
3686
|
+
}
|
|
3687
|
+
const seam = seamFor(framework, {
|
|
3688
|
+
alias: decisions.alias,
|
|
3689
|
+
srcDir: srcPrefix(root),
|
|
3690
|
+
schemaFile: decisions.schemaFile
|
|
3691
|
+
});
|
|
3692
|
+
const outdated = outdatedRuntimeWarning(root);
|
|
3693
|
+
if (seam.kind === "none") {
|
|
3694
|
+
return { target: "seam", action: "info", text: "No injection seam needed", note: seam.reason };
|
|
3695
|
+
}
|
|
3696
|
+
if (seam.kind === "instruct") {
|
|
3697
|
+
return {
|
|
3698
|
+
target: "seam",
|
|
3699
|
+
action: "info",
|
|
3700
|
+
text: "Place the injection seam",
|
|
3701
|
+
note: withWarning(seam.instruction, outdated)
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
const alsoNote = writeAlso(root, seam.also);
|
|
3705
|
+
const file = join2(root, ...seam.file.split("/"));
|
|
3706
|
+
const baseNotes = [...seam.notes, ...alsoNote === void 0 ? [] : [alsoNote]];
|
|
3707
|
+
const notes = baseNotes.length === 0 ? "" : `
|
|
3708
|
+
${baseNotes.map((n) => ` ${n}`).join("\n")}`;
|
|
3709
|
+
if (existsSync2(file)) {
|
|
3710
|
+
return {
|
|
3711
|
+
target: "seam",
|
|
3712
|
+
action: "info",
|
|
3713
|
+
text: `Add the injection seam to ${seam.file}`,
|
|
3714
|
+
note: withWarning(`${seam.ifPresent}${notes}`, outdated)
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
mkdirSync(dirname3(file), { recursive: true });
|
|
3718
|
+
writeFileSync2(file, seam.content, "utf8");
|
|
3719
|
+
return {
|
|
3720
|
+
target: "seam",
|
|
3721
|
+
action: outdated === void 0 ? "created" : "info",
|
|
3722
|
+
text: `Wrote ${seam.file} (runs the injection before your app)`,
|
|
3723
|
+
...notes === "" && outdated === void 0 ? {} : { note: withWarning(notes.trimStart(), outdated) }
|
|
3724
|
+
};
|
|
3725
|
+
}
|
|
3726
|
+
function writeAlso(root, also) {
|
|
3727
|
+
if (also === void 0) {
|
|
3728
|
+
return void 0;
|
|
3729
|
+
}
|
|
3730
|
+
const file = join2(root, ...also.file.split("/"));
|
|
3731
|
+
if (existsSync2(file)) {
|
|
3732
|
+
return also.ifPresent;
|
|
3733
|
+
}
|
|
3734
|
+
mkdirSync(dirname3(file), { recursive: true });
|
|
3735
|
+
writeFileSync2(file, also.content, "utf8");
|
|
3736
|
+
return `Wrote ${also.file} to register it.`;
|
|
3737
|
+
}
|
|
3738
|
+
function withWarning(note, warning) {
|
|
3739
|
+
if (warning === void 0) {
|
|
3740
|
+
return note;
|
|
3741
|
+
}
|
|
3742
|
+
return note === "" ? warning : `${note}
|
|
3743
|
+
\u26A0 ${warning}`;
|
|
3744
|
+
}
|
|
3745
|
+
function scaffold(root, fields, draft, decisions = DEFAULT_DECISIONS, framework = detectFramework(root)?.name) {
|
|
3746
|
+
const steps = [
|
|
3485
3747
|
ensurePenvDir(root),
|
|
3486
3748
|
writeSchemaFile(root, fields, draft, decisions),
|
|
3487
3749
|
writeConfigFile(root, decisions),
|
|
3488
3750
|
writeTsconfigAlias(root, decisions),
|
|
3489
3751
|
writeGitignore(root, decisions)
|
|
3490
3752
|
];
|
|
3753
|
+
const seam = writeSeam(root, decisions, framework);
|
|
3754
|
+
return seam === void 0 ? steps : [...steps, seam];
|
|
3491
3755
|
}
|
|
3492
3756
|
function runInit(options) {
|
|
3493
3757
|
const root = resolve4(options.cwd);
|
|
3494
3758
|
const decisions = options.decisions ?? planInit(root).decisions;
|
|
3495
|
-
return { root, decisions, steps: scaffold(root, [], false, decisions) };
|
|
3759
|
+
return { root, decisions, steps: scaffold(root, [], false, decisions, options.framework) };
|
|
3496
3760
|
}
|
|
3497
3761
|
function renderInit(result2) {
|
|
3498
3762
|
const steps = result2.steps.map((step) => {
|
|
3499
|
-
const glyph = step.action === "conflicted" ? WARN : CHECK;
|
|
3763
|
+
const glyph = step.action === "conflicted" ? WARN : step.action === "info" ? "\u2192" : CHECK;
|
|
3500
3764
|
return step.note === void 0 ? { glyph, text: step.text } : { glyph, text: step.text, note: step.note };
|
|
3501
3765
|
});
|
|
3502
3766
|
return [
|
|
@@ -3559,7 +3823,15 @@ var initCommand = defineCommand9({
|
|
|
3559
3823
|
if (!asked) {
|
|
3560
3824
|
write([...plan2.notes, ""]);
|
|
3561
3825
|
}
|
|
3562
|
-
write(
|
|
3826
|
+
write(
|
|
3827
|
+
renderInit(
|
|
3828
|
+
runInit({
|
|
3829
|
+
cwd: root,
|
|
3830
|
+
decisions,
|
|
3831
|
+
...plan2.detected && { framework: plan2.detected.name }
|
|
3832
|
+
})
|
|
3833
|
+
)
|
|
3834
|
+
);
|
|
3563
3835
|
});
|
|
3564
3836
|
}
|
|
3565
3837
|
});
|
|
@@ -3729,6 +4001,9 @@ function environmentNamed(file, explicit) {
|
|
|
3729
4001
|
function decisionsOf(config, cwd) {
|
|
3730
4002
|
return {
|
|
3731
4003
|
environments: config.environments,
|
|
4004
|
+
// `import` re-scaffolds env.ts for an existing project; injection is an init
|
|
4005
|
+
// choice, so it is not turned on here.
|
|
4006
|
+
inject: false,
|
|
3732
4007
|
schemaFile: schemaFileOf3(config),
|
|
3733
4008
|
publicPrefixes: config.publicPrefixes ?? [],
|
|
3734
4009
|
alias: detectAlias(cwd)
|