diff --git a/README.md b/README.md index b56a47e..5229e39 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ To choose from three configuration settings, install the [`eslint-config-lwc`](h | [lwc/valid-wire](./docs/rules/valid-wire.md) | validate `wire` decorator usage | | | [lwc/no-restricted-browser-globals-during-ssr](./docs/rules/no-restricted-browser-globals-during-ssr.md) | disallow access to global browser APIs during SSR | | | [lwc/no-unsupported-ssr-properties](./docs/rules/no-unsupported-ssr-properties.md) | disallow access of unsupported properties in SSR | | +| [lwc/no-node-env-in-ssr](./docs/rules/no-node-env-in-ssr.md) | disallow usage of process.env.NODE_ENV in SSR | | ### Best practices diff --git a/docs/rules/no-node-env-in-ssr.md b/docs/rules/no-node-env-in-ssr.md new file mode 100644 index 0000000..76fb60d --- /dev/null +++ b/docs/rules/no-node-env-in-ssr.md @@ -0,0 +1,33 @@ +# Disallow use of `process.env.NODE_ENV` during SSR (`lwc/no-node-env-in-ssr`) + +Using process.env.NODE_ENV during server-side rendering in JavaScript is not recommended because it can introduce unexpected behavior and bugs in your application. This environment variable is typically used for conditional logic related to development or production builds, which is more relevant on the client side. + +## Rule Details + +Example of **incorrect** code for this rule: + +```js +import { LightningElement } from 'lwc'; + +export default class Foo extends LightningElement { + connectedCallback() { + if (process.env.NODE_ENV !== 'production') { + console.log('Foo:connectedCallback'); + } + } +} +``` + +Examples of **correct** code for this rule: + +```js +import { LightningElement } from 'lwc'; + +export default class Foo extends LightningElement { + connectedCallback() { + if (!import.meta.env.SSR && process.env.NODE_ENV !== 'production') { + console.log('Foo:connectedCallback'); + } + } +} +``` diff --git a/lib/index.js b/lib/index.js index 500eea7..2ff3a01 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,6 +30,7 @@ const rules = { 'valid-wire': require('./rules/valid-wire'), 'no-restricted-browser-globals-during-ssr': require('./rules/no-restricted-browser-globals-during-ssr'), 'no-unsupported-ssr-properties': require('./rules/no-unsupported-ssr-properties'), + 'no-node-env-in-ssr': require('./rules/no-node-env-in-ssr'), }; module.exports = { diff --git a/lib/rule-helpers.js b/lib/rule-helpers.js index 593e3c2..58c9b94 100644 --- a/lib/rule-helpers.js +++ b/lib/rule-helpers.js @@ -212,3 +212,44 @@ module.exports.noPropertyAccessDuringSSR = function noPropertyAccessDuringSSR( }, }; }; + +module.exports.noNodeEnvInSSR = function noNodeEnvInSSR(context) { + const { + withinLWCVisitors, + isInsideReachableFunction, + isInsideReachableMethod, + isInsideSkippedBlock, + } = reachableDuringSSRPartial(); + + return { + ...withinLWCVisitors, + MemberExpression: (node) => { + if ( + (!isInsideReachableMethod() && + !isInsideReachableFunction() && + !inModuleScope(node, context)) || + isInsideSkippedBlock() + ) { + return; + } + if ( + node.property.type === 'Identifier' && + node.property.name === 'NODE_ENV' && + node.object.type === 'MemberExpression' && + node.object.object && + node.object.object.type === 'Identifier' && + node.object.object.name === 'process' && + node.object.property.type === 'Identifier' && + node.object.property.name === 'env' + ) { + context.report({ + node, + messageId: 'nodeEnvFound', + data: { + identifier: node.property.name, + }, + }); + } + }, + }; +}; diff --git a/lib/rules/no-node-env-in-ssr.js b/lib/rules/no-node-env-in-ssr.js new file mode 100644 index 0000000..6b88753 --- /dev/null +++ b/lib/rules/no-node-env-in-ssr.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +'use strict'; + +const { noNodeEnvInSSR } = require('../rule-helpers'); +const { docUrl } = require('../util/doc-url'); + +module.exports = { + meta: { + type: 'problem', + docs: { + url: docUrl('no-node-env-in-ssr'), + category: 'LWC', + description: 'disallow access of process.env.NODE_ENV in SSR', + }, + schema: [], + messages: { + nodeEnvFound: 'process.env.NODE_ENV is unsupported in SSR.', + }, + }, + create: (context) => { + return noNodeEnvInSSR(context); + }, +}; diff --git a/test/lib/rules/no-node-env-ssr.js b/test/lib/rules/no-node-env-ssr.js new file mode 100644 index 0000000..0f10774 --- /dev/null +++ b/test/lib/rules/no-node-env-ssr.js @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2018, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ +'use strict'; +const { RuleTester } = require('eslint'); + +const { ESLINT_TEST_CONFIG } = require('../shared'); +const rule = require('../../../lib/rules/no-node-env-in-ssr'); + +const tester = new RuleTester(ESLINT_TEST_CONFIG); + +tester.run('no-node-env-in-ssr', rule, { + valid: [ + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + // we can't use process.env.NODE_ENV here + } + renderedCallback() { + if (process.env.NODE_ENV === 'development') { + console.log('test'); + } + } + bar() { + if (process.env.NODE_ENV === 'development') { + console.log('test'); + } + } + } + `, + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + // we can't use process.env.NODE_ENV here + } + renderedCallback() { + this.bar(); + } + bar() { + if (process.env.NODE_ENV === 'development') { + console.log('test'); + } + } + } + `, + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + // we can't use process.env.NODE_ENV here + } + bar() { + doSomething(process.emv.NODE_ENV); + } + } + `, + }, + ], + invalid: [ + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + if (process.env.NODE_ENV === 'development') { + console.log('test'); + } + } + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + this.foo(); + } + foo() { + if (process.env.NODE_ENV === 'development') { + console.log('test'); + } + } + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + doSomethingWith(process.env.NODE_ENV); + } + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + this.foo(); + } + renderedCallback() { + this.foo(); + } + foo() { + doSomethingWith(process.env.NODE_ENV); + } + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + this.foo(); + } + renderedCallback() { + this.foo(); + } + foo() { + doSomethingWith(process.env.NODE_ENV); + } + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + connectedCallback() { + this.foo(); + } + renderedCallback() { + this.foo(); + } + foo() { + doSomethingWith(process.env.NODE_ENV); + } + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + constructor() { + if (process.env.NODE_ENV === 'development') { + console.log('test'); + } + } + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + constructor() { + this.foo(); + } + foo() { + doSomethingWith(process.env.NODE_ENV); + } + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + foo = process.env.NODE_ENV; + } + `, + errors: [ + { + messageId: 'nodeEnvFound', + }, + ], + }, + ], +}); diff --git a/test/lib/rules/no-restricted-browser-globals-during-ssr.js b/test/lib/rules/no-restricted-browser-globals-during-ssr.js index 561cb6b..51e3b43 100644 --- a/test/lib/rules/no-restricted-browser-globals-during-ssr.js +++ b/test/lib/rules/no-restricted-browser-globals-during-ssr.js @@ -465,5 +465,38 @@ tester.run('no-browser-globals-during-ssr', rule, { }, ], }, + { + code: ` + import { LightningElement } from 'lwc'; + import tmplA from './a.html'; + + export default class Foo extends LightningElement { + constructor() { + console.log(window.x); + } + } + `, + errors: [ + { + messageId: 'prohibitedBrowserAPIUsage', + data: { identifier: 'window' }, + }, + ], + }, + { + code: ` + import { LightningElement } from 'lwc'; + + export default class Foo extends LightningElement { + foo = window.x; + } + `, + errors: [ + { + messageId: 'prohibitedBrowserAPIUsage', + data: { identifier: 'window' }, + }, + ], + }, ], });