mnfst-run 1.0.18 → 1.0.20

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/serve.mjs +48 -25
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-run",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Zero-dependency dev server for Manifest projects",
5
5
  "type": "module",
6
6
  "bin": {
package/serve.mjs CHANGED
@@ -123,13 +123,23 @@ const LIVE_RELOAD_SCRIPT = `<script>
123
123
  })();
124
124
  \x3c/script>`;
125
125
 
126
- // Read a dotenv-style file from the project root and return a plain object.
127
- // Used to populate `window.env` in served HTML so manifest.json's `${VAR}`
128
- // placeholders resolve at runtime matching the documented developer-facing
129
- // behaviour without requiring a build step. Returns {} when the file is
130
- // absent or fails to parse.
126
+ // Read a dotenv-style file from the project root and return two maps: `public`
127
+ // (vars eligible to ship to the browser) and `private` (vars kept server-side).
128
+ // Only the public map is injected into `window.env`; the private map is logged
129
+ // at startup so devs can see what was withheld, but never reaches HTML.
131
130
  //
132
- // Supported subset (intentionally minimal, no expansion / multiline / etc.):
131
+ // Public/private split is by name prefix matching the established convention
132
+ // (Astro `PUBLIC_`, SvelteKit `PUBLIC_`, Vite `VITE_`, Next `NEXT_PUBLIC_`):
133
+ // - PUBLIC_FOO=… → exposed via window.env.PUBLIC_FOO
134
+ // - MANIFEST_API_KEY=…, STRIPE_SECRET=…, anything else → server-side only
135
+ //
136
+ // Rationale: prior versions injected the entire .env, so the scaffold's own
137
+ // MANIFEST_API_KEY (which create-starter writes with a "treat like a password"
138
+ // comment) was visible in view-source on every served page. The prefix gate
139
+ // makes the rule explicit at the call site rather than relying on devs to know
140
+ // that .env values reach the browser.
141
+ //
142
+ // Supported parse subset (intentionally minimal, no expansion / multiline):
133
143
  // - KEY=value (whitespace around `=` ok)
134
144
  // - KEY="quoted" / KEY='…' (surrounding quotes stripped)
135
145
  // - # comments and blank lines ignored
@@ -142,8 +152,9 @@ const LIVE_RELOAD_SCRIPT = `<script>
142
152
  // by the host. See the Appwrite setup doc for the full pattern.
143
153
  function loadEnvFile(rootDir) {
144
154
  const envPath = join(rootDir, '.env');
145
- if (!existsSync(envPath)) return {};
146
- const env = {};
155
+ if (!existsSync(envPath)) return { public: {}, private: [] };
156
+ const publicEnv = {};
157
+ const privateNames = [];
147
158
  try {
148
159
  const text = readFileSync(envPath, 'utf8');
149
160
  for (const line of text.split(/\r?\n/)) {
@@ -158,22 +169,24 @@ function loadEnvFile(rootDir) {
158
169
  (value.startsWith("'") && value.endsWith("'"))) {
159
170
  value = value.slice(1, -1);
160
171
  }
161
- env[key] = value;
172
+ if (key.startsWith('PUBLIC_')) publicEnv[key] = value;
173
+ else privateNames.push(key);
162
174
  }
163
175
  } catch (error) {
164
176
  console.warn('[mnfst-run] Failed to parse .env:', error.message);
165
177
  }
166
- return env;
178
+ return { public: publicEnv, private: privateNames };
167
179
  }
168
180
 
169
- // Build a `<script>window.env = {…};</script>` tag from a parsed env map.
170
- // Returns '' when there are no vars (so the injection is a no-op for projects
171
- // without a .env). Escapes any `</script` substring inside string values so an
172
- // env value can't break out of the script tag.
173
- function buildEnvInjectScript(envVars) {
174
- const keys = Object.keys(envVars);
181
+ // Build a `<script>window.env = {…};</script>` tag from the public env map.
182
+ // Returns '' when there are no public vars (so the injection is a no-op for
183
+ // projects whose .env contains only server-side secrets). Escapes any
184
+ // `</script` substring inside string values so an env value can't break out
185
+ // of the script tag.
186
+ function buildEnvInjectScript(publicEnv) {
187
+ const keys = Object.keys(publicEnv);
175
188
  if (keys.length === 0) return '';
176
- const json = JSON.stringify(envVars).replace(/<\/script/gi, '<\\/script');
189
+ const json = JSON.stringify(publicEnv).replace(/<\/script/gi, '<\\/script');
177
190
  return `<script>window.env = ${json};</script>`;
178
191
  }
179
192
 
@@ -331,13 +344,22 @@ const root = resolve(process.cwd(), dir);
331
344
 
332
345
  // Load .env from the serving root (if present) and pre-build the inject
333
346
  // script. Kept as a single string so serveFile doesn't re-stringify on every
334
- // HTML response. Empty string when no .env exists — the injection step
335
- // becomes a no-op for projects that don't use env vars.
336
- const envVars = loadEnvFile(root);
337
- const envInjectScript = buildEnvInjectScript(envVars);
338
- const envCount = Object.keys(envVars).length;
339
- if (envCount > 0) {
340
- console.log(`Loaded ${envCount} env var(s) from .env into window.env`);
347
+ // HTML response. Empty string when no public vars exist — the injection step
348
+ // becomes a no-op for projects whose .env holds only server-side secrets.
349
+ const { public: publicEnv, private: privateEnvNames } = loadEnvFile(root);
350
+ const envInjectScript = buildEnvInjectScript(publicEnv);
351
+ const publicCount = Object.keys(publicEnv).length;
352
+ if (publicCount > 0) {
353
+ console.log(`Loaded ${publicCount} PUBLIC_ env var(s) into window.env`);
354
+ }
355
+ if (privateEnvNames.length > 0) {
356
+ // Loud about what was withheld so devs notice when something they expected
357
+ // in the browser is server-side only — and so a misplaced PUBLIC_ prefix is
358
+ // obvious from the startup log.
359
+ console.log(
360
+ `[mnfst-run] ${privateEnvNames.length} non-PUBLIC_ var(s) NOT injected ` +
361
+ `into window.env (kept server-side): ${privateEnvNames.join(', ')}`
362
+ );
341
363
  }
342
364
 
343
365
  // Dedup: if a server is already serving this exact root, point the user at
@@ -467,11 +489,12 @@ function serveFile(res, filePath) {
467
489
  // Only inject into full HTML documents — not component fragments
468
490
  const isFullDoc = /<!doctype\s/i.test(html) || /<html[\s>]/i.test(html);
469
491
  if (isFullDoc) {
470
- // 1) Inject window.env into <head> (when .env present) so the
492
+ // 1) Inject window.env into <head> (when public env vars exist) so the
471
493
  // framework's manifest.json env-var substitution can resolve
472
494
  // `${VAR}` placeholders before any plugin reads the manifest.
473
495
  // Must come BEFORE framework scripts execute — <head> insertion
474
496
  // guarantees that ordering regardless of where script tags sit.
497
+ // ONLY PUBLIC_-prefixed vars are eligible; see loadEnvFile().
475
498
  let injected = html;
476
499
  if (envInjectScript) {
477
500
  injected = injected.includes('</head>')