@varlock/nextjs-integration 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -4,12 +4,15 @@ This package helps you integrate [varlock](https://varlock.dev) into a [Next.js]
4
4
 
5
5
  It is designed as a drop-in replacement for [`@next/env`](https://www.npmjs.com/package/@next/env), which is the internal package that Next.js uses to load `.env` files, as well as a small plugin for your `next.config.*` file that enables additional security features.
6
6
 
7
+ It supports Next.js v14+, including full support for both **Next.js 15** and **16** with both **Webpack** and **Turbopack** bundlers.
8
+
7
9
  Compared to the default `@next/env` behavior, this package provides:
8
10
 
9
11
  - validation of your env vars against your `.env.schema`
10
12
  - type-generation and type-safe env var access with built-in docs
11
13
  - redaction of sensitive values from application logs
12
14
  - leak detection and prevention, both at build and runtime
15
+ - sourcemap scrubbing to prevent secrets leaking in production source maps
13
16
  - more flexible multi-env handling - you can load env-specific files other than `.env.development`/`.env.production`
14
17
 
15
18
  See [our docs site](https://varlock.dev/integrations/nextjs/) for complete installation and usage instructions.
package/dist/loader.js CHANGED
@@ -12,6 +12,9 @@ var require_loader = __commonJS({
12
12
  if (!process.env.DEBUG_VARLOCK_NEXT_INTEGRATION) return;
13
13
  console.log("[varlock-loader]", ...args);
14
14
  }
15
+ function isTurbopackWorker() {
16
+ return !!(process.env.TURBOPACK || process.env.TURBOPACK_DEV || process.env.TURBOPACK_BUILD || process.env.npm_config_turbopack);
17
+ }
15
18
  function escapeRegExp(str) {
16
19
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
17
20
  }
@@ -27,7 +30,6 @@ ${source.slice(match[1].length)}`;
27
30
  ${source}`;
28
31
  }
29
32
  function webpackLoader(source) {
30
- this.cacheable(false);
31
33
  const projectRoot = this.rootContext || process.cwd();
32
34
  if (!this.resourcePath.startsWith(projectRoot)) {
33
35
  return source;
@@ -38,10 +40,7 @@ ${source}`;
38
40
  }
39
41
  debug("processing:", relPath);
40
42
  const isClientComponent = USE_CLIENT_RE.test(source);
41
- if (isClientComponent) {
42
- debug("skipping client component:", relPath);
43
- return source;
44
- }
43
+ if (isClientComponent) debug("client component:", relPath);
45
44
  const rawEnv = process.env.__VARLOCK_ENV;
46
45
  if (!rawEnv) {
47
46
  throw new Error("expected __VARLOCK_ENV to be set");
@@ -52,22 +51,25 @@ ${source}`;
52
51
  } catch {
53
52
  return source;
54
53
  }
55
- let result = source;
56
54
  const loaderOptions = this.getOptions?.() ?? {};
57
55
  const isWebpack = loaderOptions.bundler === "webpack";
58
- const isTurbopack = loaderOptions.bundler === "turbopack";
56
+ const isTurbopack = loaderOptions.bundler === "turbopack" || isTurbopackWorker();
59
57
  const isEdge = loaderOptions.isEdge ?? false;
60
- let initGuard;
61
- if (isEdge) {
62
- initGuard = "if(globalThis.__varlockPatchConsole)globalThis.__varlockPatchConsole();";
63
- } else {
64
- initGuard = "if(!globalThis.__varlockBuildInit){globalThis.__varlockBuildInit=true;require('varlock/env').initVarlockEnv();require('varlock/patch-console').patchGlobalConsole();}";
65
- if (isWebpack) {
66
- initGuard += "require('varlock/patch-console').patchGlobalConsole();";
58
+ let result = source;
59
+ if (!isClientComponent) {
60
+ let initGuard;
61
+ if (isEdge) {
62
+ initGuard = "if(globalThis.__varlockPatchConsole)globalThis.__varlockPatchConsole();";
63
+ } else {
64
+ initGuard = "if(!globalThis.__varlockBuildInit){globalThis.__varlockBuildInit=true;require('varlock/env').initVarlockEnv();require('varlock/patch-console').patchGlobalConsole();}";
65
+ if (isWebpack) {
66
+ initGuard += "require('varlock/patch-console').patchGlobalConsole();";
67
+ }
67
68
  }
69
+ result = prependAfterDirectives(result, initGuard);
68
70
  }
69
- result = prependAfterDirectives(result, initGuard);
70
71
  if (isTurbopack && source.includes("ENV.")) {
72
+ this.cacheable(false);
71
73
  for (const [key, item] of Object.entries(envGraph.config)) {
72
74
  if (item.isSensitive) continue;
73
75
  const pattern = new RegExp(`\\bENV\\.${escapeRegExp(key)}(?![\\w$])`, "g");
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/loader.ts"],"names":["exports"],"mappings":"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","file":"loader.js","sourcesContent":["import type { SerializedEnvGraph } from 'varlock';\n\nfunction debug(...args: Array<any>) {\n if (!process.env.DEBUG_VARLOCK_NEXT_INTEGRATION) return;\n // eslint-disable-next-line no-console\n console.log('[varlock-loader]', ...args);\n}\n\ntype LoaderContext = {\n cacheable(flag: boolean): void;\n resourcePath: string;\n rootContext: string;\n getOptions?(): { bundler?: 'webpack' | 'turbopack'; isEdge?: boolean };\n};\n\nfunction escapeRegExp(str: string) {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// detect 'use client' directive at top of file (before any code, after optional comments)\n// uses [^\\S\\n]* (horizontal whitespace) instead of \\s* to avoid exponential backtracking\nconst USE_CLIENT_RE = /^(?:[^\\S\\n]*\\/\\/[^\\n]*\\n|[^\\S\\n]*\\/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*\\/[^\\S\\n]*\\n|[^\\S\\n]*\\n)*[^\\S\\n]*['\"]use client['\"]/;\n\n// match directive prologue ('use server', 'use client', etc.) + optional trailing semicolons/newlines\n// captures the directive block so we can inject code after it\nconst DIRECTIVE_PROLOGUE_RE = /^((?:[^\\S\\n]*\\/\\/[^\\n]*\\n|[^\\S\\n]*\\/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*\\/[^\\S\\n]*\\n|[^\\S\\n]*\\n)*[^\\S\\n]*['\"]use (?:server|client|strict)['\"][^\\S\\n]*;?[^\\S\\n]*\\n?)/;\n\n/** Prepend code after any directive prologue (e.g. 'use server') to avoid breaking it */\nfunction prependAfterDirectives(source: string, codeToPrepend: string): string {\n const match = source.match(DIRECTIVE_PROLOGUE_RE);\n if (match) {\n return `${match[1] + codeToPrepend}\\n${source.slice(match[1].length)}`;\n }\n return `${codeToPrepend}\\n${source}`;\n}\n\n/**\n * Webpack/Turbopack loader that:\n * 1. Injects resolved env config into instrumentation and proxy files.\n * 2. Replaces `ENV.KEY` references for non-sensitive vars with literal JSON values.\n *\n * SECURITY: Sensitive env values are ONLY embedded into server-side files\n * (never client components). The static ENV.KEY replacements explicitly skip\n * sensitive vars.\n */\nfunction webpackLoader(this: LoaderContext, source: string) {\n // disable caching so we always re-run when env values change\n this.cacheable(false); // TODO: probably dont want this?\n\n // only transform files within the project root\n // this skips node_modules AND symlinked workspace packages (e.g. varlock/env)\n // which turbopack resolves to their real paths outside node_modules\n const projectRoot = this.rootContext || process.cwd();\n if (!this.resourcePath.startsWith(projectRoot)) {\n return source;\n }\n // still skip node_modules within the project root\n const relPath = this.resourcePath.slice(projectRoot.length);\n if (relPath.includes('/node_modules/') || relPath.includes('\\\\node_modules\\\\')) {\n return source;\n }\n\n debug('processing:', relPath);\n\n // skip client components — patches use node builtins (zlib, http) that don't work in browser\n const isClientComponent = USE_CLIENT_RE.test(source);\n if (isClientComponent) {\n debug('skipping client component:', relPath);\n return source;\n }\n\n const rawEnv = process.env.__VARLOCK_ENV;\n if (!rawEnv) {\n throw new Error('expected __VARLOCK_ENV to be set');\n }\n\n // TODO: avoid parsing on every file\n let envGraph: SerializedEnvGraph;\n try {\n envGraph = JSON.parse(rawEnv);\n } catch {\n return source;\n }\n\n let result = source;\n\n // Inject a tiny guarded init snippet into every server file.\n // Pre-rendering workers receive compiled code via IPC (not from disk), so runtime\n // file injection doesn't help them. This ensures initVarlockEnv() and\n // patchGlobalConsole() run once per process — the globalThis guard makes it\n // idempotent, and turbopack deduplicates the require() targets.\n const loaderOptions = this.getOptions?.() ?? {};\n\n const isWebpack = loaderOptions.bundler === 'webpack';\n const isTurbopack = loaderOptions.bundler === 'turbopack';\n const isEdge = loaderOptions.isEdge ?? false;\n\n let initGuard: string;\n if (isEdge) {\n // Edge compilation: can't use require(), so use globalThis.__varlockPatchConsole\n // which the init-edge bundle exposes. The init guard is skipped since edge init\n // is handled by the runtime file injection (processAssets hook).\n initGuard = 'if(globalThis.__varlockPatchConsole)globalThis.__varlockPatchConsole();';\n } else {\n initGuard = 'if(!globalThis.__varlockBuildInit){globalThis.__varlockBuildInit=true;require(\\'varlock/env\\').initVarlockEnv();require(\\'varlock/patch-console\\').patchGlobalConsole();}';\n // When used from webpack, React wraps console for RSC dev replay AFTER our initial\n // patch in the runtime file. Re-patching outside the once-guard ensures our redaction\n // wraps React's wrapper so secrets are redacted before React captures them.\n // patchGlobalConsole() no-ops if console.log still has _varlockPatchedFn.\n if (isWebpack) {\n initGuard += 'require(\\'varlock/patch-console\\').patchGlobalConsole();';\n }\n }\n result = prependAfterDirectives(result, initGuard);\n\n // static replacements for non-sensitive env vars\n // webpack uses DefinePlugin for this, so only needed for turbopack\n if (isTurbopack && source.includes('ENV.')) {\n for (const [key, item] of Object.entries(envGraph.config)) {\n if (item.isSensitive) continue;\n\n // TODO: smarter replacement (vite version uses AST?)\n\n // match ENV.KEY as a member expression (word boundary before ENV, not followed by more identifier chars)\n const pattern = new RegExp(`\\\\bENV\\\\.${escapeRegExp(key)}(?![\\\\w$])`, 'g');\n result = result.replace(pattern, JSON.stringify(item.value));\n }\n }\n\n return result;\n}\n\n// CJS export required for webpack loader-runner compatibility\nmodule.exports = webpackLoader;\n"]}
1
+ {"version":3,"sources":["../src/loader.ts"],"names":["exports"],"mappings":"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","file":"loader.js","sourcesContent":["import type { SerializedEnvGraph } from 'varlock';\n\nfunction debug(...args: Array<any>) {\n if (!process.env.DEBUG_VARLOCK_NEXT_INTEGRATION) return;\n // eslint-disable-next-line no-console\n console.log('[varlock-loader]', ...args);\n}\n\ntype LoaderContext = {\n cacheable(flag: boolean): void;\n resourcePath: string;\n rootContext: string;\n getOptions?(): { bundler?: 'webpack' | 'turbopack'; isEdge?: boolean };\n};\n\nfunction isTurbopackWorker() {\n return !!(\n process.env.TURBOPACK\n || process.env.TURBOPACK_DEV\n || process.env.TURBOPACK_BUILD\n || process.env.npm_config_turbopack\n );\n}\n\nfunction escapeRegExp(str: string) {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// detect 'use client' directive at top of file (before any code, after optional comments)\n// uses [^\\S\\n]* (horizontal whitespace) instead of \\s* to avoid exponential backtracking\nconst USE_CLIENT_RE = /^(?:[^\\S\\n]*\\/\\/[^\\n]*\\n|[^\\S\\n]*\\/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*\\/[^\\S\\n]*\\n|[^\\S\\n]*\\n)*[^\\S\\n]*['\"]use client['\"]/;\n\n// match directive prologue ('use server', 'use client', etc.) + optional trailing semicolons/newlines\n// captures the directive block so we can inject code after it\nconst DIRECTIVE_PROLOGUE_RE = /^((?:[^\\S\\n]*\\/\\/[^\\n]*\\n|[^\\S\\n]*\\/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*\\/[^\\S\\n]*\\n|[^\\S\\n]*\\n)*[^\\S\\n]*['\"]use (?:server|client|strict)['\"][^\\S\\n]*;?[^\\S\\n]*\\n?)/;\n\n/** Prepend code after any directive prologue (e.g. 'use server') to avoid breaking it */\nfunction prependAfterDirectives(source: string, codeToPrepend: string): string {\n const match = source.match(DIRECTIVE_PROLOGUE_RE);\n if (match) {\n return `${match[1] + codeToPrepend}\\n${source.slice(match[1].length)}`;\n }\n return `${codeToPrepend}\\n${source}`;\n}\n\n/**\n * Webpack/Turbopack loader that:\n * 1. Injects resolved env config into instrumentation and proxy files.\n * 2. Replaces `ENV.KEY` references for non-sensitive vars with literal JSON values.\n *\n * SECURITY: Sensitive env values are ONLY embedded into server-side files\n * (never client components). The static ENV.KEY replacements explicitly skip\n * sensitive vars.\n */\nfunction webpackLoader(this: LoaderContext, source: string) {\n // only transform files within the project root\n // this skips node_modules AND symlinked workspace packages (e.g. varlock/env)\n // which turbopack resolves to their real paths outside node_modules\n const projectRoot = this.rootContext || process.cwd();\n if (!this.resourcePath.startsWith(projectRoot)) {\n return source;\n }\n // still skip node_modules within the project root\n const relPath = this.resourcePath.slice(projectRoot.length);\n if (relPath.includes('/node_modules/') || relPath.includes('\\\\node_modules\\\\')) {\n return source;\n }\n\n debug('processing:', relPath);\n\n const isClientComponent = USE_CLIENT_RE.test(source);\n if (isClientComponent) debug('client component:', relPath);\n\n const rawEnv = process.env.__VARLOCK_ENV;\n if (!rawEnv) {\n throw new Error('expected __VARLOCK_ENV to be set');\n }\n\n // TODO: avoid parsing on every file\n let envGraph: SerializedEnvGraph;\n try {\n envGraph = JSON.parse(rawEnv);\n } catch {\n return source;\n }\n\n const loaderOptions = this.getOptions?.() ?? {};\n\n const isWebpack = loaderOptions.bundler === 'webpack';\n const isTurbopack = loaderOptions.bundler === 'turbopack' || isTurbopackWorker();\n const isEdge = loaderOptions.isEdge ?? false;\n\n let result = source;\n\n if (!isClientComponent) {\n // Inject a tiny guarded init snippet into every server file.\n // Pre-rendering workers receive compiled code via IPC (not from disk), so runtime\n // file injection doesn't help them. This ensures initVarlockEnv() and\n // patchGlobalConsole() run once per process — the globalThis guard makes it\n // idempotent, and turbopack deduplicates the require() targets.\n let initGuard: string;\n if (isEdge) {\n // Edge compilation: can't use require(), so use globalThis.__varlockPatchConsole\n // which the init-edge bundle exposes. The init guard is skipped since edge init\n // is handled by the runtime file injection (processAssets hook).\n initGuard = 'if(globalThis.__varlockPatchConsole)globalThis.__varlockPatchConsole();';\n } else {\n initGuard = 'if(!globalThis.__varlockBuildInit){globalThis.__varlockBuildInit=true;require(\\'varlock/env\\').initVarlockEnv();require(\\'varlock/patch-console\\').patchGlobalConsole();}';\n // When used from webpack, React wraps console for RSC dev replay AFTER our initial\n // patch in the runtime file. Re-patching outside the once-guard ensures our redaction\n // wraps React's wrapper so secrets are redacted before React captures them.\n // patchGlobalConsole() no-ops if console.log still has _varlockPatchedFn.\n if (isWebpack) {\n initGuard += 'require(\\'varlock/patch-console\\').patchGlobalConsole();';\n }\n }\n result = prependAfterDirectives(result, initGuard);\n }\n\n // static replacements for non-sensitive env vars\n // webpack uses DefinePlugin for this, so only needed for turbopack\n if (isTurbopack && source.includes('ENV.')) {\n // disable caching only for files that reference ENV — their output depends on env values\n this.cacheable(false);\n for (const [key, item] of Object.entries(envGraph.config)) {\n if (item.isSensitive) continue;\n\n // TODO: smarter replacement (vite version uses AST?)\n\n // match ENV.KEY as a member expression (word boundary before ENV, not followed by more identifier chars)\n const pattern = new RegExp(`\\\\bENV\\\\.${escapeRegExp(key)}(?![\\\\w$])`, 'g');\n result = result.replace(pattern, JSON.stringify(item.value));\n }\n }\n\n return result;\n}\n\n// CJS export required for webpack loader-runner compatibility\nmodule.exports = webpackLoader;\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@varlock/nextjs-integration",
3
3
  "description": "drop-in replacement for @next/env that uses varlock to load .env files with validation and extra security features",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/dmno-dev/varlock.git",
@@ -42,13 +42,13 @@
42
42
  "node": ">=22"
43
43
  },
44
44
  "peerDependencies": {
45
- "varlock": "^0.6.0",
45
+ "varlock": "^0.7.0",
46
46
  "next": ">=14"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "25.3.2",
50
50
  "tsup": "^8.5.1",
51
- "varlock": "^0.6.0",
51
+ "varlock": "^0.7.0",
52
52
  "vitest": "^4.0.18"
53
53
  }
54
54
  }