import * as globals from 'globals' import omit from 'lodash/omit' import { GlideAPIs } from '@servicenow/glide/util' import type { Rule, Linter, Scope } from 'eslint' import { ReferenceTracker } from '@eslint-community/eslint-utils' const BROWSER_API_WARNING = 'Web APIs are not supported by the now platform' const GLIDE_API_WARNING = 'Glide APIs are not supported by the now platform when used from a 3rd party dependency' const NO_GLOBAL_THIS = 'ES2020 `globalThis` variable is not supported by the now platform' export type LogLevel = 'warn' | 'error' type ESLintPropertyRule = { message: string object?: string property?: string name?: string } const glideAPIs = GlideAPIs const globalsAllowList: string[] = ['console', 'fetch'] const coreNodeModules = [ 'assert', 'buffer', 'child_process', 'cluster', 'crypto', 'dgram', 'dns', 'domain', 'events', 'freelist', 'fs', 'http', 'https', 'module', 'net', 'os', 'path', 'punycode', 'querystring', 'readline', 'repl', 'smalloc', 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'tracing', 'tty', 'url', 'util', 'vm', 'zlib', ] const getNodeGlobals = () => Object.keys(globals.nodeBuiltin).filter((key) => !globalsAllowList.includes(key)) function createTrackMap(isNodeGlobals: boolean): Record { const trackMapItems = isNodeGlobals ? getNodeGlobals() : coreNodeModules const trackMap: Record = {} for (const item of trackMapItems) { trackMap[item] = { [ReferenceTracker.READ]: { name: item, }, } } return trackMap } /** * Custom rule: disallow unsupported Node.js built-in APIs. * Replaces @servicenow/eslint-plugin-sdk-app-plugin/no-unsupported-node-builtins. */ const noUnsupportedNodeBuiltins: Rule.RuleModule = { meta: { docs: { description: 'disallow unsupported Node.js built-in APIs', }, messages: { forbidden: 'The {{name}} Node.js API is not supported in now platform.', }, schema: [], type: 'problem', }, create(context) { return { // biome-ignore lint/suspicious/noExplicitAny: ESLint rule listener typing 'Program:exit'(node: any) { const sourceCode = context.sourceCode const tracker = new ReferenceTracker(sourceCode.getScope(node) as Scope.Scope, { mode: 'legacy' }) const trackMap = createTrackMap(false) const globalsTrackMap = createTrackMap(true) const references = [ ...tracker.iterateCjsReferences(trackMap), ...tracker.iterateEsmReferences(trackMap), ...tracker.iterateGlobalReferences(globalsTrackMap), ] for (const { node: refNode, info } of references) { context.report({ node: refNode as Rule.Node, messageId: 'forbidden', data: info as Record, }) } }, } }, } /** * Custom rule: disallow dynamic import() syntax. * Replaces eslint-plugin-es-x/no-dynamic-import. */ const noDynamicImport: Rule.RuleModule = { meta: { docs: { description: 'disallow dynamic import() syntax', }, messages: { forbidden: "Dynamic 'import()' syntax is not supported on the now platform.", }, schema: [], type: 'problem', }, create(context) { return { ImportExpression(node: Rule.Node) { context.report({ node, messageId: 'forbidden' }) }, } }, } export const getPluginRules = (): Record => { return { 'no-dynamic-import': noDynamicImport, 'no-unsupported-node-builtins': noUnsupportedNodeBuiltins, } } const getRestrictedGlobals = () => { const globalRules: ESLintPropertyRule[] = [{ name: 'globalThis', message: NO_GLOBAL_THIS }] const browserRules = Object.keys(globals.browser) .filter((key) => !globalsAllowList.includes(key)) .map((key) => { return { name: key, message: BROWSER_API_WARNING, } }) return globalRules.concat(browserRules) } const getGlideGlobalEslintRules = () => { const glideGlobals = glideAPIs.defaults return Object.keys(glideGlobals).map((glideGlobal) => { return { name: glideGlobal, message: GLIDE_API_WARNING, } }) } const getGlideNamespaceEslintRules = () => { const namespacedAPIs = omit(glideAPIs, ['defaults']) // Deduplicate rules — the Glide API data contains duplicate object.property entries // across namespaces, and ESLint v9 rejects arrays with duplicate items. const deduplicatedRuleKeys = new Set() const rules: ESLintPropertyRule[] = [] const addRule = (rule: ESLintPropertyRule) => { const key = `${rule.object}.${rule.property}` if (!deduplicatedRuleKeys.has(key)) { deduplicatedRuleKeys.add(key) rules.push(rule) } } for (const [namespace, classes] of Object.entries(namespacedAPIs)) { for (const [className, properties] of Object.entries(classes)) { addRule({ object: namespace, property: className, message: GLIDE_API_WARNING, }) for (const property of properties) { addRule({ object: className, property, message: GLIDE_API_WARNING, }) } } } return rules } /** * ESLint config for local modules — Rhino compatibility rules only (no Glide restrictions). * Local modules can use Glide APIs since they run in the ServiceNow scope. */ export const getLocalModuleEslintConfig = (level: LogLevel = 'error'): Linter.LegacyConfig => { const pluginRules = getPluginRules() const restrictedGlobals = getRestrictedGlobals() const rules: Linter.LegacyConfig['rules'] = { ...Object.fromEntries(Object.keys(pluginRules).map((name) => [name, level])), 'no-restricted-globals': [level, ...restrictedGlobals], } return { env: { node: true, }, parserOptions: { ecmaVersion: 'latest', sourceType: 'module', }, rules, } } /** * ESLint config for 3rd party dependencies — Rhino compatibility + Glide API restrictions. * 3rd party code must not use Glide APIs (they are platform-internal). */ export const getEslintConfig = (level: LogLevel = 'warn'): Linter.LegacyConfig => { const pluginRules = getPluginRules() const restrictedGlobals = getRestrictedGlobals() const rules = { ...Object.fromEntries(Object.keys(pluginRules).map((name) => [name, level])), ...getDependencyConfig(level, restrictedGlobals), } return { env: { node: true, }, parserOptions: { ecmaVersion: 'latest', sourceType: 'module', }, rules, } } function getDependencyConfig(level: LogLevel, restrictedGlobals: ESLintPropertyRule[]): Linter.LegacyConfig['rules'] { const globalRules = getGlideGlobalEslintRules() const namespaceRules = getGlideNamespaceEslintRules() return { 'no-restricted-modules': [ level, { paths: [{ name: '@servicenow/glide', message: GLIDE_API_WARNING }], patterns: ['@servicenow/glide/*'], }, ], 'no-restricted-imports': [ level, { paths: [{ name: '@servicenow/glide', message: GLIDE_API_WARNING }], patterns: ['@servicenow/glide/*'], }, ], 'no-restricted-globals': [level, ...globalRules, ...restrictedGlobals], 'no-restricted-properties': [level, ...namespaceRules], } }