Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/babel/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { declare } from "@babel/helper-plugin-utils";
import type babelCore from "@babel/core";
import { resolveEnv, accessor } from "../../shared";
import { resolveEnv, createAccessor } from "../../shared";
import { resolveEnvExampleKeys } from "../../shared/resolve-env-example-keys";
import { PluginOptions } from "./types";

Expand All @@ -25,6 +25,8 @@ export default declare<PluginOptions>(({ template, types }, options) => {
})()
: Object.create(null);

const accessor = createAccessor(options.accessorKey);

const replaceEnvForCompileTime = (
template: typeof babelCore.template,
property: string,
Expand Down
11 changes: 11 additions & 0 deletions packages/babel/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,15 @@ export interface PluginOptions {
* process.env.NODE_ENV === "production" ? "runtime" : "compile-time"
*/
transformMode?: "compile-time" | "runtime";

/**
* The global variable key used to access environment variables at runtime.
* This determines the property name on `globalThis` where env vars are stored.
*
* @default "import_meta_env"
* @example
* With accessorKey: "my_env", the plugin transforms:
* import.meta.env.API_URL -> Object.create(globalThis.my_env || null).API_URL
*/
accessorKey?: string;
}
22 changes: 22 additions & 0 deletions packages/cli/src/create-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import colors from "picocolors";
import { version } from "../package.json";
import { resolveOutputFileNames } from "./resolve-output-file-names";
import { defaultOutput } from "./shared";
import { DEFAULT_ACCESSOR_KEY } from "../../shared/constant";

export interface Args {
env: string;
example: string;
path: string[];
disposable: boolean;
generate?: string;
prepend?: string;
accessorKey: string;
}

export const createCommand = () =>
Expand Down Expand Up @@ -44,6 +48,19 @@ export const createCommand = () =>
"--disposable",
"Do not create backup files and restore from backup files. In local development, disable this option to avoid rebuilding the project when environment variable changes, In production, enable this option to avoid generating unnecessary backup files.",
)
.option(
"-g, --generate <filepath>",
"Generate a standalone JavaScript file containing environment variables instead of replacing placeholders in existing files.",
)
.option(
"--prepend <filepath>",
"Prepend environment variables to an existing JavaScript file (e.g., remoteEntry.js). The globalThis.<key> assignment will be inserted at the beginning of the file.",
)
.option(
"-k, --accessor-key <key>",
"The global variable key used to access environment variables (e.g., globalThis.<key>).",
DEFAULT_ACCESSOR_KEY,
)
.action((args: Args) => {
args = { ...args };

Expand All @@ -60,6 +77,11 @@ export const createCommand = () =>
envExampleFilePath: args.example,
});

// Skip output file validation for --generate and --prepend modes
if (args.generate || args.prepend) {
return;
}

const path = args.path ?? defaultOutput;
const foundOutputFilePaths = resolveOutputFileNames(path);
if (foundOutputFilePaths.length === 0) {
Expand Down
53 changes: 53 additions & 0 deletions packages/cli/src/generate-env-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { writeFileSync, readFileSync, mkdirSync, existsSync } from "fs";
import { dirname } from "path";
import serialize from "serialize-javascript";
import { DEFAULT_ACCESSOR_KEY } from "../../shared/constant";

const generateEnvContent = (
env: Record<string, string>,
accessorKey: string
): string => {
return `globalThis.${accessorKey} = ${serialize(env)};
`;
};

export const generateEnvFile = ({
filePath,
env,
accessorKey = DEFAULT_ACCESSOR_KEY,
}: {
filePath: string;
env: Record<string, string>;
accessorKey?: string;
}): void => {
// Ensure directory exists
const dir = dirname(filePath);
mkdirSync(dir, { recursive: true });

// Generate the JS content
const content = generateEnvContent(env, accessorKey);

writeFileSync(filePath, content, "utf8");
};

export const prependEnvToFile = ({
filePath,
env,
accessorKey = DEFAULT_ACCESSOR_KEY,
}: {
filePath: string;
env: Record<string, string>;
accessorKey?: string;
}): void => {
if (!existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}

const existingContent = readFileSync(filePath, "utf8");
const envContent = generateEnvContent(env, accessorKey);

// Prepend env content to existing file
const newContent = envContent + existingContent;

writeFileSync(filePath, newContent, "utf8");
};
32 changes: 32 additions & 0 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { backupFileExt, defaultOutput } from "./shared";
import { resolveOutputFileNames } from "./resolve-output-file-names";
import { replaceAllPlaceholderWithEnv } from "./replace-all-placeholder-with-env";
import { shouldInjectEnv } from "./should-inject-env";
import { generateEnvFile, prependEnvToFile } from "./generate-env-file";
import colors from "picocolors";

export const main = (di: {
Expand All @@ -22,6 +23,37 @@ export const main = (di: {
envFilePath: opts.env,
});

// Generate mode: create a standalone env.js file
if (opts.generate) {
generateEnvFile({
filePath: opts.generate,
env,
accessorKey: opts.accessorKey,
});
console.info(
colors.green(
`[import-meta-env]: Generated ${opts.generate} with accessor key "${opts.accessorKey}"`,
),
);
return;
}

// Prepend mode: prepend env vars to an existing JS file
if (opts.prepend) {
prependEnvToFile({
filePath: opts.prepend,
env,
accessorKey: opts.accessorKey,
});
console.info(
colors.green(
`[import-meta-env]: Prepended env vars to ${opts.prepend} with accessor key "${opts.accessorKey}"`,
),
);
return;
}

// Legacy mode: replace placeholders in existing files
const path = opts.path ?? defaultOutput;
let hasReplaced = false;
resolveOutputFileNames(path).forEach((outputFileName) => {
Expand Down
9 changes: 8 additions & 1 deletion packages/shared/constant.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
// 1. Accessor cannot contain `eval` as it would violate CSP.
// 2. Accessor need to fallback to empty object, since during prerender there is no environment variables in globalThis.
export const accessor = `Object.create(globalThis.import_meta_env || null)`;

export const DEFAULT_ACCESSOR_KEY = "import_meta_env";

export const createAccessor = (accessorKey = DEFAULT_ACCESSOR_KEY) =>
`Object.create(globalThis.${accessorKey} || null)`;

// Keep backward compatibility
export const accessor = createAccessor();
2 changes: 1 addition & 1 deletion packages/unplugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"rollup": "4.52.4",
"ts-node": "10.9.2",
"typescript": "5.9.3",
"vite": "6.3.6",
"vite": "6.4.0",
"webpack": "5.102.1"
},
"dependencies": {
Expand Down
5 changes: 3 additions & 2 deletions packages/unplugin/src/constant.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { accessor } from "../../shared/constant";
import { createAccessor } from "../../shared/constant";

export const createAccessorRegExp = (
suffix: string,
quote: "single" | "double" = "double",
accessorKey?: string,
) =>
new RegExp(
"\\b" +
accessor
createAccessor(accessorKey)
.replace(/\\/g, "\\\\")
.replace(/([\(\)\[\]\|])/g, "\\$1")
.replace(/\s/g, "\\s*")
Expand Down
6 changes: 5 additions & 1 deletion packages/unplugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ export const unpluginFactory: UnpluginFactory<Options> = (options, meta) => {
debug && console.debug(html);
debug && console.debug("==================");

html = html.replace(createAccessorRegExp(""), "import.meta.env");
html = html.replace(
createAccessorRegExp("", "double", options?.accessorKey),
"import.meta.env",
);

debug && console.debug("=== index.html after ===");
debug && console.debug(html);
Expand Down Expand Up @@ -206,6 +209,7 @@ export const unpluginFactory: UnpluginFactory<Options> = (options, meta) => {
example: envExampleKeys,
meta,
viteConfig,
accessorKey: options?.accessorKey,
});

debug && console.debug("=== after ===");
Expand Down
5 changes: 4 additions & 1 deletion packages/unplugin/src/transform-prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "./vite/preserve-built-in-env";
import { unwrapSignalForImportMetaEnvEnv } from "./qwik/unwrap-signal-for-import-meta-env-env";
import MagicString from "magic-string";
import { accessor } from "packages/shared";
import { createAccessor } from "packages/shared";
import { replace } from "./replace";

export function transformProd({
Expand All @@ -15,13 +15,16 @@ export function transformProd({
meta,
example,
viteConfig,
accessorKey,
}: {
code: string;
id: string;
meta: UnpluginContextMeta;
example: readonly string[];
viteConfig?: ViteResolvedConfig;
accessorKey?: string;
}) {
const accessor = createAccessor(accessorKey);
const s = new MagicString(code);

if (id.includes("node_modules") === false) {
Expand Down
11 changes: 11 additions & 0 deletions packages/unplugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,15 @@ export interface PluginOptions {
* ```
*/
transformMode?: "compile-time" | "runtime";

/**
* The global variable key used to access environment variables at runtime.
* This determines the property name on `globalThis` where env vars are stored.
*
* @default "import_meta_env"
* @example
* // With accessorKey: "my_env", the plugin transforms:
* // import.meta.env.API_URL -> Object.create(globalThis.my_env || null).API_URL
*/
accessorKey?: string;
}
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.