diff --git a/packages/plugin-vite/demo/islands/tests/EnvIsland.tsx b/packages/plugin-vite/demo/islands/tests/EnvIsland.tsx new file mode 100644 index 00000000000..389e9c2d661 --- /dev/null +++ b/packages/plugin-vite/demo/islands/tests/EnvIsland.tsx @@ -0,0 +1,18 @@ +import { useEffect, useState } from "preact/hooks"; + +export function EnvIsland() { + const [ready, setReady] = useState(false); + useEffect(() => { + setReady(true); + }, []); + + const deno = Deno.env.get("FRESH_PUBLIC_FOO"); + // deno-lint-ignore no-process-global + const nodeEnv = process.env.FRESH_PUBLIC_FOO; + + return ( +
+
{JSON.stringify({ deno,nodeEnv},null,2)}
+
+ ); +} diff --git a/packages/plugin-vite/demo/routes/tests/env.tsx b/packages/plugin-vite/demo/routes/tests/env.tsx new file mode 100644 index 00000000000..a6a7b1daeee --- /dev/null +++ b/packages/plugin-vite/demo/routes/tests/env.tsx @@ -0,0 +1,5 @@ +import { EnvIsland } from "../../islands/tests/EnvIsland.tsx"; + +export default function Page() { + return ; +} diff --git a/packages/plugin-vite/src/plugins/patches.ts b/packages/plugin-vite/src/plugins/patches.ts index 734cccc80c3..d11b902ec35 100644 --- a/packages/plugin-vite/src/plugins/patches.ts +++ b/packages/plugin-vite/src/plugins/patches.ts @@ -3,6 +3,7 @@ import * as babel from "@babel/core"; import { cjsPlugin } from "./patches/commonjs.ts"; import { jsxComments } from "./patches/jsx_comment.ts"; import babelReact from "@babel/preset-react"; +import { inlineEnvVarsPlugin } from "./patches/inline_env_vars.ts"; export function patches(): Plugin { let isDev = false; @@ -16,12 +17,8 @@ export function patches(): Plugin { return true; }, transform(code, id, options) { - if (code.includes("__commonJS") || /\.(tsx?|m[jt]s)$/.test(id)) { - return; - } - const presets = []; - if (!options?.ssr) { + if (!options?.ssr && /\.(tsx?|m[jt]s)$/.test(id)) { presets.push([babelReact, { runtime: "automatic", importSource: "preact", @@ -32,7 +29,13 @@ export function patches(): Plugin { const res = babel.transformSync(code, { filename: id, babelrc: false, - plugins: [cjsPlugin, jsxComments], + plugins: [ + cjsPlugin, + jsxComments, + inlineEnvVarsPlugin( + isDev ? "development" : "production", + ), + ], presets, }); diff --git a/packages/plugin-vite/src/plugins/patches/inline_env_vars.ts b/packages/plugin-vite/src/plugins/patches/inline_env_vars.ts new file mode 100644 index 00000000000..9f0f557ce81 --- /dev/null +++ b/packages/plugin-vite/src/plugins/patches/inline_env_vars.ts @@ -0,0 +1,66 @@ +import type { NodePath, types } from "@babel/core"; + +export function inlineEnvVarsPlugin(mode: string) { + const allowed = new Map(); + for (const [name, value] of Object.entries(Deno.env.toObject())) { + if (name.startsWith("FRESH_PUBLIC_")) { + allowed.set(name, value); + } + } + + allowed.set("NODE_ENV", Deno.env.get("NODE_ENV") ?? mode); + + return ( + { types: t }: { types: typeof types }, + ): babel.PluginObj => { + function replace(path: NodePath, name: string) { + if (allowed.has(name)) { + const value = allowed.get(name); + + if (value !== undefined) { + path.replaceWith(t.stringLiteral(value)); + } else { + path.replaceWith(t.identifier("undefined")); + } + } + } + + return { + name: "fresh-env-var", + visitor: { + MemberExpression(path) { + // Check: process.env.* + if ( + t.isMemberExpression(path.node.object) && + t.isIdentifier(path.node.object.object) && + path.node.object.object.name === "process" && + t.isIdentifier(path.node.object.property) && + path.node.object.property.name === "env" && + t.isIdentifier(path.node.property) + ) { + const name = path.node.property.name; + replace(path, name); + } + }, + CallExpression(path) { + // Check: Deno.env.get("") + if ( + t.isMemberExpression(path.node.callee) && + t.isMemberExpression(path.node.callee.object) && + t.isIdentifier(path.node.callee.object.object) && + path.node.callee.object.object.name === "Deno" && + t.isIdentifier(path.node.callee.object.property) && + path.node.callee.object.property.name === "env" && + t.isIdentifier(path.node.callee.property) && + path.node.callee.property.name === "get" && + path.node.arguments.length > 0 && + t.isStringLiteral(path.node.arguments[0]) + ) { + const name = path.node.arguments[0].value; + replace(path, name); + } + }, + }, + }; + }; +} diff --git a/packages/plugin-vite/src/plugins/patches/inline_env_vars_test.ts b/packages/plugin-vite/src/plugins/patches/inline_env_vars_test.ts new file mode 100644 index 00000000000..da0ff1f3bba --- /dev/null +++ b/packages/plugin-vite/src/plugins/patches/inline_env_vars_test.ts @@ -0,0 +1,71 @@ +import { expect } from "@std/expect/expect"; +import * as babel from "@babel/core"; +import { inlineEnvVarsPlugin } from "./inline_env_vars.ts"; +import { usingEnv } from "../../../tests/test_utils.ts"; + +function runTest(options: { input: string; expected: string; mode?: string }) { + const res = babel.transformSync(options.input, { + filename: "foo.js", + babelrc: false, + plugins: [inlineEnvVarsPlugin(options.mode ?? "development")], + }); + + const output = res?.code ?? ""; + expect(output).toEqual(options.expected); +} + +Deno.test("env vars - inline NODE_ENV", () => { + using _ = usingEnv("NODE_ENV", "foobar"); + runTest({ + input: `() => process.env.NODE_ENV`, + expected: `() => "foobar";`, + }); +}); + +Deno.test("env vars - inline NODE_ENV mode", () => { + runTest({ + input: `() => process.env.NODE_ENV`, + expected: `() => "asdf";`, + mode: "asdf", + }); +}); + +Deno.test("env vars - inline custom process.env.*", () => { + using _ = usingEnv("FRESH_PUBLIC_FOO", "a"); + runTest({ + input: `() => process.env.FRESH_PUBLIC_FOO`, + expected: `() => "a";`, + }); +}); + +Deno.test("env vars - inline Deno.env.get()", () => { + using _ = usingEnv("FRESH_PUBLIC_FOO", "b"); + runTest({ + input: `() => Deno.env.get("FRESH_PUBLIC_FOO")`, + expected: `() => "b";`, + }); +}); + +Deno.test("env vars - inline Deno.env.get(NODE_ENV)", () => { + using _ = usingEnv("NODE_ENV", "c"); + runTest({ + input: `() => Deno.env.get("NODE_ENV")`, + expected: `() => "c";`, + }); +}); + +Deno.test("env vars - inline Deno.env.get(NODE_ENV) mode", () => { + runTest({ + input: `() => Deno.env.get("NODE_ENV")`, + expected: `() => "test";`, + mode: "test", + }); +}); + +Deno.test("env vars - inline const _ = Deno.env.get()", () => { + using _ = usingEnv("FRESH_PUBLIC_FOO", "test"); + runTest({ + input: `const deno = Deno.env.get("FRESH_PUBLIC_FOO");`, + expected: `const deno = "test";`, + }); +}); diff --git a/packages/plugin-vite/tests/dev_server_test.ts b/packages/plugin-vite/tests/dev_server_test.ts index 64c43b9c772..0f18fe2424d 100644 --- a/packages/plugin-vite/tests/dev_server_test.ts +++ b/packages/plugin-vite/tests/dev_server_test.ts @@ -144,3 +144,26 @@ Deno.test({ sanitizeResources: false, sanitizeOps: false, }); + +Deno.test({ + name: "vite dev - inline env vars", + fn: async () => { + await withDevServer(DEMO_DIR, async (address) => { + await withBrowser(async (page) => { + await page.goto(`${address}/tests/env`, { + waitUntil: "networkidle2", + }); + await page.locator(".ready").wait(); + + const res = await page.locator("pre").evaluate((el) => + // deno-lint-ignore no-explicit-any + (el as any).textContent ?? "" + ); + + expect(JSON.parse(res)).toEqual({ deno: "foobar", nodeEnv: "foobar" }); + }); + }, { FRESH_PUBLIC_FOO: "foobar" }); + }, + sanitizeResources: false, + sanitizeOps: false, +}); diff --git a/packages/plugin-vite/tests/test_utils.ts b/packages/plugin-vite/tests/test_utils.ts index 73e1cde9bd3..faa5bbfbe91 100644 --- a/packages/plugin-vite/tests/test_utils.ts +++ b/packages/plugin-vite/tests/test_utils.ts @@ -50,6 +50,7 @@ async function copyDir(from: string, to: string) { export async function withDevServer( fixtureDir: string, fn: (address: string, dir: string) => void | Promise, + env: Record = {}, ) { await using tmp = await withTmpDir({ dir: path.join(import.meta.dirname!, ".."), @@ -72,7 +73,7 @@ export default defineConfig({ ); await withChildProcessServer( - { cwd: tmp.dir, args: ["run", "-A", "npm:vite", "--port", "0"] }, + { cwd: tmp.dir, args: ["run", "-A", "npm:vite", "--port", "0"], env }, async (address) => await fn(address, tmp.dir), ); } @@ -110,3 +111,12 @@ export async function buildVite(fixtureDir: string) { }, }; } + +export function usingEnv(name: string, value: string) { + Deno.env.set(name, value); + return { + [Symbol.dispose]: () => { + Deno.env.delete(name); + }, + }; +}