diff --git a/lib/cmd/vite/plugins/client-env.js b/lib/cmd/vite/plugins/client-env.js index 967df2a..b744867 100644 --- a/lib/cmd/vite/plugins/client-env.js +++ b/lib/cmd/vite/plugins/client-env.js @@ -3,6 +3,45 @@ const fs = require('fs-extra'); const ENV_VAR_RE = /process\.env\.([A-Z_][A-Z0-9_]*)/g; +const ENV_DESTRUCTURE_RE = /\{([\s\S]*?)\}\s*=\s*process\.env\b/g; + +/** + * Extract env var names from object-destructuring assignments on process.env. + * + * Supports forms like: + * const { FOO, BAR } = process.env; + * const { FOO: fooAlias, BAR = 'x' } = process.env; + * + * Any token that does not start with a valid all-caps env identifier + * ([A-Z_][A-Z0-9_]*) is ignored. + * + * @param {string} code + * @returns {string[]} + */ +function collectDestructuredEnvNames(code) { + const names = []; + + for (const match of code.matchAll(ENV_DESTRUCTURE_RE)) { + const rawFields = match[1] + .split(',') + .map(s => s.trim()) + .filter(Boolean); + + for (const rawField of rawFields) { + const field = rawField + .replace(/^\.\.\./, '') // rest syntax + .split(':')[0] // alias syntax + .split('=')[0] // default-value syntax + .trim(); + + if (/^[A-Z_][A-Z0-9_]*$/.test(field)) { + names.push(field); + } + } + } + + return names; +} /** * Factory for a shared process.env reference collector used across Rollup @@ -81,6 +120,10 @@ function createClientEnvCollector(outputPath) { for (const match of code.matchAll(ENV_VAR_RE)) { found.add(match[1]); } + + for (const varName of collectDestructuredEnvNames(code)) { + found.add(varName); + } return null; } }; @@ -108,4 +151,4 @@ function createClientEnvCollector(outputPath) { return { plugin, write }; } -module.exports = { createClientEnvCollector }; +module.exports = { createClientEnvCollector, collectDestructuredEnvNames }; diff --git a/lib/cmd/vite/plugins/client-env.test.js b/lib/cmd/vite/plugins/client-env.test.js index b6a7ba1..03811f0 100644 --- a/lib/cmd/vite/plugins/client-env.test.js +++ b/lib/cmd/vite/plugins/client-env.test.js @@ -6,7 +6,7 @@ const path = require('path'); const os = require('os'); const fs = require('fs'); -const { createClientEnvCollector } = require('./client-env'); +const { createClientEnvCollector, collectDestructuredEnvNames } = require('./client-env'); // ── helpers ─────────────────────────────────────────────────────────────────── @@ -206,4 +206,51 @@ describe('createClientEnvCollector', () => { expect(readJson(out)).toEqual(['GOOGLE_PLACES_API_KEY', 'NODE_ENV', 'SENTRY_DSN']); }); + + it('collects vars from process.env destructuring', async () => { + const out = tmpOutputPath(); + const collector = createClientEnvCollector(out); + const p = collector.plugin(); + + p.transform('const { CORAL_GRAPHQL_TOKEN, CORAL_HOST_URL } = process.env;'); + await collector.write(); + + expect(readJson(out)).toEqual(['CORAL_GRAPHQL_TOKEN', 'CORAL_HOST_URL']); + }); + + it('collects destructured vars with aliases and defaults', async () => { + const out = tmpOutputPath(); + const collector = createClientEnvCollector(out); + const p = collector.plugin(); + + p.transform("const { SWIFTYPE_API_KEY: token, SWIFTYPE_HOST = 'https://x' } = process.env;"); + await collector.write(); + + expect(readJson(out)).toEqual(['SWIFTYPE_API_KEY', 'SWIFTYPE_HOST']); + }); + + it('ignores non-uppercase tokens in destructuring', async () => { + const out = tmpOutputPath(); + const collector = createClientEnvCollector(out); + const p = collector.plugin(); + + p.transform('const { not_env, lowerCaseVar, GOOD_ENV } = process.env;'); + await collector.write(); + + expect(readJson(out)).toEqual(['GOOD_ENV']); + }); +}); + +describe('collectDestructuredEnvNames', () => { + it('returns names from simple destructuring', () => { + const code = 'const { FOO, BAR } = process.env;'; + + expect(collectDestructuredEnvNames(code)).toEqual(['FOO', 'BAR']); + }); + + it('handles aliases, defaults and rest syntax', () => { + const code = 'const { FOO: fooAlias, BAR = \'x\', ...REST } = process.env;'; + + expect(collectDestructuredEnvNames(code)).toEqual(['FOO', 'BAR', 'REST']); + }); });