mnfst-run 1.0.13 → 1.0.14

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 +80 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-run",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "Zero-dependency dev server for Manifest projects",
5
5
  "type": "module",
6
6
  "bin": {
package/serve.mjs CHANGED
@@ -115,6 +115,60 @@ const LIVE_RELOAD_SCRIPT = `<script>
115
115
  })();
116
116
  \x3c/script>`;
117
117
 
118
+ // Read a dotenv-style file from the project root and return a plain object.
119
+ // Used to populate `window.env` in served HTML so manifest.json's `${VAR}`
120
+ // placeholders resolve at runtime — matching the documented developer-facing
121
+ // behaviour without requiring a build step. Returns {} when the file is
122
+ // absent or fails to parse.
123
+ //
124
+ // Supported subset (intentionally minimal, no expansion / multiline / etc.):
125
+ // - KEY=value (whitespace around `=` ok)
126
+ // - KEY="quoted" / KEY='…' (surrounding quotes stripped)
127
+ // - # comments and blank lines ignored
128
+ // - lines without `=` ignored
129
+ //
130
+ // Production note: env injection happens ONLY through this dev server.
131
+ // Static deploys (Netlify/Vercel/Cloudflare Pages/S3/etc.) serve manifest.json
132
+ // verbatim, so any `${VAR}` placeholder that needs a value in production must
133
+ // be hardcoded in manifest.json, baked in at prerender time, or substituted
134
+ // by the host. See the Appwrite setup doc for the full pattern.
135
+ function loadEnvFile(rootDir) {
136
+ const envPath = join(rootDir, '.env');
137
+ if (!existsSync(envPath)) return {};
138
+ const env = {};
139
+ try {
140
+ const text = readFileSync(envPath, 'utf8');
141
+ for (const line of text.split(/\r?\n/)) {
142
+ const trimmed = line.trim();
143
+ if (!trimmed || trimmed.startsWith('#')) continue;
144
+ const eqIdx = trimmed.indexOf('=');
145
+ if (eqIdx === -1) continue;
146
+ const key = trimmed.slice(0, eqIdx).trim();
147
+ if (!key) continue;
148
+ let value = trimmed.slice(eqIdx + 1).trim();
149
+ if ((value.startsWith('"') && value.endsWith('"')) ||
150
+ (value.startsWith("'") && value.endsWith("'"))) {
151
+ value = value.slice(1, -1);
152
+ }
153
+ env[key] = value;
154
+ }
155
+ } catch (error) {
156
+ console.warn('[mnfst-run] Failed to parse .env:', error.message);
157
+ }
158
+ return env;
159
+ }
160
+
161
+ // Build a `<script>window.env = {…};</script>` tag from a parsed env map.
162
+ // Returns '' when there are no vars (so the injection is a no-op for projects
163
+ // without a .env). Escapes any `</script` substring inside string values so an
164
+ // env value can't break out of the script tag.
165
+ function buildEnvInjectScript(envVars) {
166
+ const keys = Object.keys(envVars);
167
+ if (keys.length === 0) return '';
168
+ const json = JSON.stringify(envVars).replace(/<\/script/gi, '<\\/script');
169
+ return `<script>window.env = ${json};</script>`;
170
+ }
171
+
118
172
  // --- CLI args ---
119
173
  const args = process.argv.slice(2);
120
174
  let dir = '.';
@@ -253,6 +307,17 @@ if (listMode) {
253
307
 
254
308
  const root = resolve(process.cwd(), dir);
255
309
 
310
+ // Load .env from the serving root (if present) and pre-build the inject
311
+ // script. Kept as a single string so serveFile doesn't re-stringify on every
312
+ // HTML response. Empty string when no .env exists — the injection step
313
+ // becomes a no-op for projects that don't use env vars.
314
+ const envVars = loadEnvFile(root);
315
+ const envInjectScript = buildEnvInjectScript(envVars);
316
+ const envCount = Object.keys(envVars).length;
317
+ if (envCount > 0) {
318
+ console.log(`Loaded ${envCount} env var(s) from .env into window.env`);
319
+ }
320
+
256
321
  // Dedup: if a server is already serving this exact root, point the user at
257
322
  // it (and open the browser, since that's what they were going to do anyway).
258
323
  const existing = await findRunningServer(root);
@@ -377,9 +442,21 @@ function serveFile(res, filePath) {
377
442
  // Only inject into full HTML documents — not component fragments
378
443
  const isFullDoc = /<!doctype\s/i.test(html) || /<html[\s>]/i.test(html);
379
444
  if (isFullDoc) {
380
- const injected = html.includes('</body>')
381
- ? html.replace('</body>', LIVE_RELOAD_SCRIPT + '</body>')
382
- : html + LIVE_RELOAD_SCRIPT;
445
+ // 1) Inject window.env into <head> (when .env present) so the
446
+ // framework's manifest.json env-var substitution can resolve
447
+ // `${VAR}` placeholders before any plugin reads the manifest.
448
+ // Must come BEFORE framework scripts execute — <head> insertion
449
+ // guarantees that ordering regardless of where script tags sit.
450
+ let injected = html;
451
+ if (envInjectScript) {
452
+ injected = injected.includes('</head>')
453
+ ? injected.replace('</head>', envInjectScript + '</head>')
454
+ : envInjectScript + injected;
455
+ }
456
+ // 2) Inject the live-reload script before </body> (or at end).
457
+ injected = injected.includes('</body>')
458
+ ? injected.replace('</body>', LIVE_RELOAD_SCRIPT + '</body>')
459
+ : injected + LIVE_RELOAD_SCRIPT;
383
460
  body = Buffer.from(injected, 'utf8');
384
461
  }
385
462
  }