-
Notifications
You must be signed in to change notification settings - Fork 8
[APPS] Add @datadog/eslint-plugin-apps with valid-connections-file rule #332
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: sdkennedy2/apps-emit-connections-manifest
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # `@datadog/eslint-plugin-apps` | ||
|
|
||
| ESLint rules for [Datadog High Code Apps](https://docs.datadoghq.com/service_management/app_builder/). | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| npm install --save-dev @datadog/eslint-plugin-apps | ||
| ``` | ||
|
|
||
| Requires `eslint >= 8.57.0`. | ||
|
|
||
| ## Usage — flat config (`eslint.config.js`, ESLint v9) | ||
|
|
||
| ```js | ||
| import apps from '@datadog/eslint-plugin-apps'; | ||
| import tsParser from '@typescript-eslint/parser'; | ||
|
|
||
| export default [ | ||
| ...apps.configs.recommended, | ||
| { | ||
| files: ['connections.{ts,tsx,js,jsx}'], | ||
| languageOptions: { parser: tsParser }, | ||
| }, | ||
| ]; | ||
| ``` | ||
|
|
||
| ## Usage — legacy config (`.eslintrc`) | ||
|
|
||
| ```json | ||
| { | ||
| "extends": ["plugin:@datadog/apps/recommended-legacy"], | ||
| "parser": "@typescript-eslint/parser" | ||
| } | ||
| ``` | ||
|
|
||
| ## Rules | ||
|
|
||
| ### `valid-connections-file` | ||
|
|
||
| Validates the project-root `connections.ts` file the Datadog vite plugin reads at build time. Mirrors the build plugin's structural requirements so authors see violations in their editor instead of as a build error. | ||
|
|
||
| The rule checks that the file: | ||
|
|
||
| - Defines exactly one top-level `export const CONNECTIONS = { … }` | ||
| - Uses an object literal for the initializer (not a function call) | ||
| - Does not use spread elements or computed keys inside the object | ||
| - Uses static string values (string literals or interpolation-free template literals — no env-var lookups, identifiers, concatenation, etc.) | ||
|
|
||
| The rule auto-scopes by filename — it only runs on files whose basename matches `connections.{ts,tsx,js,jsx}`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| { | ||
| "name": "@datadog/eslint-plugin-apps", | ||
| "packageManager": "yarn@4.0.2", | ||
| "version": "3.1.4", | ||
| "license": "MIT", | ||
| "author": "Datadog", | ||
| "description": "ESLint rules for Datadog High Code Apps", | ||
| "keywords": [ | ||
| "datadog", | ||
| "eslint", | ||
| "eslint-plugin", | ||
| "apps", | ||
| "high-code-apps" | ||
| ], | ||
| "homepage": "https://github.com/DataDog/build-plugins#readme", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/DataDog/build-plugins", | ||
| "directory": "packages/published/eslint-plugin-apps" | ||
| }, | ||
| "main": "./dist/src/index.js", | ||
| "module": "./dist/src/index.mjs", | ||
| "types": "./dist/src/index.d.ts", | ||
| "exports": { | ||
| "./dist/src": "./dist/src/index.js", | ||
| "./dist/src/*": "./dist/src/*", | ||
| ".": "./src/index.ts" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "types": "./dist/src/index.d.ts", | ||
| "exports": { | ||
| "./package.json": "./package.json", | ||
| ".": { | ||
| "import": "./dist/src/index.mjs", | ||
| "require": "./dist/src/index.js", | ||
| "types": "./dist/src/index.d.ts" | ||
| } | ||
| } | ||
| }, | ||
| "files": [ | ||
| "dist" | ||
| ], | ||
| "scripts": { | ||
| "buildCmd": "rollup --config rollup.config.mjs", | ||
| "build": "yarn clean && yarn buildCmd", | ||
| "clean": "rm -rf dist", | ||
| "prepack": "yarn build", | ||
| "typecheck": "tsc --noEmit", | ||
| "watch": "yarn build --watch" | ||
| }, | ||
| "devDependencies": { | ||
| "@dd/tools": "workspace:*", | ||
| "@rollup/plugin-json": "6.1.0", | ||
| "@rollup/plugin-node-resolve": "15.3.0", | ||
| "@types/eslint": "9.6.1", | ||
| "@types/estree": "1.0.8", | ||
| "@typescript-eslint/parser": "7.5.0", | ||
| "esbuild": "0.25.8", | ||
| "eslint": "8.57.0", | ||
| "rollup": "4.45.1", | ||
| "rollup-plugin-dts": "6.1.1", | ||
| "rollup-plugin-esbuild": "6.1.1", | ||
| "typescript": "5.4.3" | ||
| }, | ||
| "peerDependencies": { | ||
| "eslint": ">=8.57.0" | ||
| }, | ||
|
Comment on lines
+66
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For the documented Useful? React with 👍 / 👎. |
||
| "buildPlugin": { | ||
| "hideFromRootReadme": true | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| // Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
| // Copyright 2019-Present Datadog, Inc. | ||
|
|
||
| import json from '@rollup/plugin-json'; | ||
| import { nodeResolve } from '@rollup/plugin-node-resolve'; | ||
| import dts from 'rollup-plugin-dts'; | ||
| import esbuild from 'rollup-plugin-esbuild'; | ||
|
|
||
| import packageJson from './package.json' with { type: 'json' }; | ||
|
|
||
| // `@dd/tools/rollupConfig.mjs` is bundler-plugin specific (it parses the | ||
| // package name expecting a `-plugin` suffix and feeds unplugin assumptions | ||
| // into the build), so we hand-roll a minimal config here. | ||
|
|
||
| const external = [ | ||
| ...Object.keys(packageJson.peerDependencies ?? {}), | ||
| ...Object.keys(packageJson.dependencies ?? {}), | ||
| // Externalize ESLint and Node built-ins. | ||
| 'eslint', | ||
| 'node:path', | ||
| 'path', | ||
| ]; | ||
|
|
||
| const input = 'src/index.ts'; | ||
|
|
||
| export default [ | ||
| { | ||
| input, | ||
| external, | ||
| output: { | ||
| file: 'dist/src/index.mjs', | ||
| format: 'es', | ||
| sourcemap: true, | ||
| }, | ||
| plugins: [ | ||
| json(), | ||
| nodeResolve({ preferBuiltins: true }), | ||
| esbuild({ target: 'node18' }), | ||
| ], | ||
| }, | ||
| { | ||
| input, | ||
| external, | ||
| output: { | ||
| file: 'dist/src/index.js', | ||
| format: 'cjs', | ||
| sourcemap: true, | ||
| // Rollup's CJS emit for an `export default` source already produces | ||
| // `module.exports = plugin` directly (no `__esModule` wrapping), so | ||
| // legacy `.eslintrc` `require()` returns the plugin directly. The | ||
| // `footer` collapse trick used with esbuild isn't needed here. | ||
| }, | ||
| plugins: [ | ||
| json(), | ||
| nodeResolve({ preferBuiltins: true }), | ||
| esbuild({ target: 'node18' }), | ||
| ], | ||
| }, | ||
| { | ||
| input, | ||
| external, | ||
| output: { | ||
| file: 'dist/src/index.d.ts', | ||
| format: 'es', | ||
| }, | ||
| plugins: [dts()], | ||
| }, | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| // Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
| // Copyright 2019-Present Datadog, Inc. | ||
|
|
||
| import type { ESLint, Linter, Rule } from 'eslint'; | ||
|
|
||
| import validConnectionsFile from './rules/valid-connections-file'; | ||
|
|
||
| const PLUGIN_NAME = 'apps'; | ||
| const PLUGIN_PACKAGE = '@datadog/eslint-plugin-apps'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This hard-coded version will not be updated by the repo's Useful? React with 👍 / 👎. |
||
| const VERSION = '3.1.4'; | ||
|
|
||
| const rules: Record<string, Rule.RuleModule> = { | ||
| 'valid-connections-file': validConnectionsFile, | ||
| }; | ||
|
|
||
| interface AppsPlugin extends ESLint.Plugin { | ||
| meta: { name: string; version: string }; | ||
| rules: Record<string, Rule.RuleModule>; | ||
| configs: { | ||
| recommended: Linter.Config[]; | ||
| 'recommended-legacy': Linter.LegacyConfig; | ||
| }; | ||
| } | ||
|
|
||
| const plugin: AppsPlugin = { | ||
| meta: { name: PLUGIN_PACKAGE, version: VERSION }, | ||
| rules, | ||
| configs: { | ||
| recommended: [], | ||
| 'recommended-legacy': { | ||
| plugins: ['@datadog/apps'], | ||
| rules: { | ||
| '@datadog/apps/valid-connections-file': 'error', | ||
| }, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| plugin.configs.recommended = [ | ||
| { | ||
| plugins: { [PLUGIN_NAME]: plugin }, | ||
| rules: { | ||
| [`${PLUGIN_NAME}/valid-connections-file`]: 'error', | ||
| }, | ||
| }, | ||
| ]; | ||
|
|
||
| export default plugin; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| // Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
| // Copyright 2019-Present Datadog, Inc. | ||
|
|
||
| import { RuleTester } from 'eslint'; | ||
|
|
||
| import rule from './valid-connections-file'; | ||
|
|
||
| // ESLint 8 RuleTester uses the legacy `parser` + `parserOptions` shape (string | ||
| // path to the parser module, parserOptions at top level), not the v9 flat | ||
| // `languageOptions.parser`. The repo's @types/eslint is v9 so the legacy | ||
| // shape isn't typed; cast through `unknown` to keep the tests honest about | ||
| // runtime. The published plugin also supports v9 flat config at runtime; this | ||
| // test only validates the rule logic against the AST shape @typescript-eslint | ||
| // /parser produces. | ||
| const ruleTester = new RuleTester({ | ||
| parser: require.resolve('@typescript-eslint/parser'), | ||
| parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, | ||
| } as unknown as ConstructorParameters<typeof RuleTester>[0]); | ||
|
|
||
| const filename = 'connections.ts'; | ||
|
|
||
| ruleTester.run('valid-connections-file', rule, { | ||
| valid: [ | ||
| { | ||
| name: 'CONNECTIONS with string-literal values', | ||
| filename, | ||
| code: `export const CONNECTIONS = { OPEN_AI: '00000000-0000-0000-0000-000000000000' } as const;`, | ||
| }, | ||
| { | ||
| name: 'template literal without interpolation is allowed', | ||
| filename, | ||
| code: `export const CONNECTIONS = { A: \`abc\` } as const;`, | ||
| }, | ||
| { | ||
| name: 'string literal key is allowed', | ||
| filename, | ||
| code: `export const CONNECTIONS = { 'open-ai': 'uuid-1' } as const;`, | ||
| }, | ||
| { | ||
| name: 'out-of-scope file is ignored even when malformed', | ||
| filename: 'src/something.ts', | ||
| code: `export const OTHER = makeConnections();`, | ||
| }, | ||
| ], | ||
| invalid: [ | ||
| { | ||
| name: 'missing CONNECTIONS export', | ||
| filename, | ||
| code: `export const OTHER = { foo: 'bar' } as const;`, | ||
| errors: [{ messageId: 'missingExport' }], | ||
| }, | ||
| { | ||
| name: 'lowercase `connections` is not accepted', | ||
| filename, | ||
| code: `export const connections = { foo: 'a' } as const;`, | ||
| errors: [{ messageId: 'missingExport' }], | ||
| }, | ||
| { | ||
| name: 'CONNECTIONS not initialized with object literal', | ||
| filename, | ||
| code: `export const CONNECTIONS = makeConnections();`, | ||
| errors: [{ messageId: 'notObjectLiteral' }], | ||
| }, | ||
| { | ||
| name: 'duplicate CONNECTIONS exports', | ||
| filename, | ||
| code: `export const CONNECTIONS = { A: 'x' } as const; | ||
| export const CONNECTIONS = { B: 'y' } as const;`, | ||
| errors: [{ messageId: 'duplicateExport' }], | ||
| }, | ||
| { | ||
| name: 'spread element in CONNECTIONS', | ||
| filename, | ||
| code: `const other = { B: 'y' }; | ||
| export const CONNECTIONS = { ...other, A: 'x' } as const;`, | ||
| errors: [{ messageId: 'spreadElement' }], | ||
| }, | ||
| { | ||
| name: 'computed key in CONNECTIONS', | ||
| filename, | ||
| code: `const k = 'A'; | ||
| export const CONNECTIONS = { [k]: 'x' } as const;`, | ||
| errors: [{ messageId: 'computedKey' }], | ||
| }, | ||
| { | ||
| name: 'value is an identifier (env-like reference)', | ||
| filename, | ||
| code: `declare const ENV_ID: string; | ||
| export const CONNECTIONS = { A: ENV_ID } as const;`, | ||
| errors: [ | ||
| { | ||
| messageId: 'valueNotStaticString', | ||
| data: { key: 'A', valueType: 'Identifier' }, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| name: 'value is a binary expression', | ||
| filename, | ||
| code: `export const CONNECTIONS = { A: 'a' + 'b' } as const;`, | ||
| errors: [ | ||
| { | ||
| messageId: 'valueNotStaticString', | ||
| data: { key: 'A', valueType: 'BinaryExpression' }, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| name: 'value is a template literal with interpolation', | ||
| filename, | ||
| code: `declare const id: string; | ||
| export const CONNECTIONS = { A: \`prefix-\${id}\` } as const;`, | ||
| errors: [ | ||
| { | ||
| messageId: 'templateInterpolation', | ||
| data: { key: 'A' }, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a fresh consumer project following this install command, both documented configs immediately reference
@typescript-eslint/parser(the flat config imports it and the legacy config names it), but this package only declares that parser as a devDependency, so it is not installed or peer-checked for consumers. This makes the README setup fail with a missing parser as soon as users lint a TypeScriptconnections.ts; include the parser in the install instructions or declare an appropriate peer/optional peer.Useful? React with 👍 / 👎.