[middleware] support destructuring for env vars in static analysis (#37556)

This commit enables the following patterns in Middleware:

```ts
// with a dot notation
const { ENV_VAR, "ENV-VAR": myEnvVar } = process.env;

// or with an object access
const { ENV_VAR2, "ENV-VAR2": myEnvVar2 } = process["env"];
```

### Related

- @cramforce asked this fixed here: https://github.com/vercel/next.js/pull/37514#discussion_r892437257



## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`
This commit is contained in:
Gal Schlezinger 2022-06-09 14:49:58 +03:00 committed by GitHub
parent 76083210ed
commit 9155201807
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 7 deletions

View file

@ -164,18 +164,26 @@ function getCodeAnalizer(params: {
})
}
/**
* Declares an environment variable that is being used in this module
* through this static analysis.
*/
const addUsedEnvVar = (envVarName: string) => {
const buildInfo = getModuleBuildInfo(parser.state.module)
if (buildInfo.nextUsedEnvVars === undefined) {
buildInfo.nextUsedEnvVars = new Set()
}
buildInfo.nextUsedEnvVars.add(envVarName)
}
/**
* A handler for calls to `process.env` where we identify the name of the
* ENV variable being assigned and store it in the module info.
*/
const handleCallMemberChain = (_: unknown, members: string[]) => {
if (members.length >= 2 && members[0] === 'env') {
const buildInfo = getModuleBuildInfo(parser.state.module)
if (buildInfo.nextUsedEnvVars === undefined) {
buildInfo.nextUsedEnvVars = new Set()
}
buildInfo.nextUsedEnvVars.add(members[1])
addUsedEnvVar(members[1])
if (!isInMiddlewareLayer(parser)) {
return true
}
@ -226,6 +234,37 @@ function getCodeAnalizer(params: {
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain)
hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
/**
* Support static analyzing environment variables through
* destructuring `process.env` or `process["env"]`:
*
* const { MY_ENV, "MY-ENV": myEnv } = process.env
* ^^^^^^ ^^^^^^
*/
hooks.declarator.tap(NAME, (declarator) => {
if (
declarator.init?.type === 'MemberExpression' &&
isProcessEnvMemberExpression(declarator.init) &&
declarator.id?.type === 'ObjectPattern'
) {
for (const property of declarator.id.properties) {
if (property.type === 'RestElement') continue
if (
property.key.type === 'Literal' &&
typeof property.key.value === 'string'
) {
addUsedEnvVar(property.key.value)
} else if (property.key.type === 'Identifier') {
addUsedEnvVar(property.key.name)
}
}
if (!isInMiddlewareLayer(parser)) {
return true
}
}
})
registerUnsupportedApiHooks(parser, compilation)
}
}
@ -538,3 +577,14 @@ function isNullLiteral(expr: any) {
function isUndefinedIdentifier(expr: any) {
return expr.name === 'undefined'
}
function isProcessEnvMemberExpression(memberExpression: any): boolean {
return (
memberExpression.object?.type === 'Identifier' &&
memberExpression.object.name === 'process' &&
((memberExpression.property?.type === 'Literal' &&
memberExpression.property.value === 'env') ||
(memberExpression.property?.type === 'Identifier' &&
memberExpression.property.name === 'env'))
)
}

View file

@ -66,6 +66,19 @@ export async function middleware(request) {
// The next line is required to allow to find the env variable
// eslint-disable-next-line no-unused-expressions
process.env.MIDDLEWARE_TEST
// The next line is required to allow to find the env variable
// eslint-disable-next-line no-unused-expressions
const { ANOTHER_MIDDLEWARE_TEST } = process.env
if (!ANOTHER_MIDDLEWARE_TEST) {
console.log('missing ANOTHER_MIDDLEWARE_TEST')
}
const { 'STRING-ENV-VAR': stringEnvVar } = process['env']
if (!stringEnvVar) {
console.log('missing STRING-ENV-VAR')
}
return serializeData(JSON.stringify({ process: { env: process.env } }))
}

View file

@ -33,6 +33,8 @@ describe('Middleware Runtime', () => {
context.buildId = 'development'
context.app = await launchApp(context.appDir, context.appPort, {
env: {
ANOTHER_MIDDLEWARE_TEST: 'asdf2',
'STRING-ENV-VAR': 'asdf3',
MIDDLEWARE_TEST: 'asdf',
NEXT_RUNTIME: 'edge',
},
@ -100,6 +102,8 @@ describe('Middleware Runtime', () => {
context.appPort = await findPort()
context.app = await nextStart(context.appDir, context.appPort, {
env: {
ANOTHER_MIDDLEWARE_TEST: 'asdf2',
'STRING-ENV-VAR': 'asdf3',
MIDDLEWARE_TEST: 'asdf',
NEXT_RUNTIME: 'edge',
},
@ -120,7 +124,7 @@ describe('Middleware Runtime', () => {
)
expect(manifest.middleware).toEqual({
'/': {
env: ['MIDDLEWARE_TEST'],
env: ['MIDDLEWARE_TEST', 'ANOTHER_MIDDLEWARE_TEST', 'STRING-ENV-VAR'],
files: ['server/edge-runtime-webpack.js', 'server/middleware.js'],
name: 'middleware',
page: '/',
@ -215,6 +219,8 @@ function tests(context, locale = '') {
expect(readMiddlewareJSON(res)).toEqual({
process: {
env: {
ANOTHER_MIDDLEWARE_TEST: 'asdf2',
'STRING-ENV-VAR': 'asdf3',
MIDDLEWARE_TEST: 'asdf',
NEXT_RUNTIME: 'edge',
},