From c5ef56031dcfc12b2cf98c37c37b7b3c779d5444 Mon Sep 17 00:00:00 2001 From: Khang Nguyen <49442382+khangsfdc@users.noreply.github.com> Date: Thu, 12 Oct 2023 19:57:24 -0400 Subject: [PATCH] feat: add no node env ssr rule (#130) * chore: add rule to disallow process.env.NODE_ENV * docs: update no-node-env-in-ssr.md * chore: add no-node-env-in-ssr to README * chore: prettier * test: add no-node-env-in-ssr * Update lib/rules/no-node-env-in-ssr.js Co-authored-by: Dale Bustad * fix: add rule-helper for no-node-env * fix: add object guard * test: add more tests * fix: typo --------- Co-authored-by: Dale Bustad --- README.md | 1 + docs/rules/no-node-env-in-ssr.md | 33 +++ lib/index.js | 1 + lib/rule-helpers.js | 41 +++ lib/rules/no-node-env-in-ssr.js | 28 ++ test/lib/rules/no-node-env-ssr.js | 258 ++++++++++++++++++ ...o-restricted-browser-globals-during-ssr.js | 33 +++ 7 files changed, 395 insertions(+) create mode 100644 docs/rules/no-node-env-in-ssr.md create mode 100644 lib/rules/no-node-env-in-ssr.js create mode 100644 test/lib/rules/no-node-env-ssr.js 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' }, + }, + ], + }, ], });