Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions packages/published/eslint-plugin-apps/README.md
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install the parser used by the documented configs

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 TypeScript connections.ts; include the parser in the install instructions or declare an appropriate peer/optional peer.

Useful? React with 👍 / 👎.

```

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}`.
72 changes: 72 additions & 0 deletions packages/published/eslint-plugin-apps/package.json
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare the TypeScript parser for consumers

For the documented connections.ts setup, both README configs require @typescript-eslint/parser, but this package only declares eslint as a peer and keeps the parser in devDependencies. In a fresh app that follows npm install --save-dev @datadog/eslint-plugin-apps, the consumer won't get this package's dev dependency, so the flat-config import or legacy parser resolution fails before the rule can run. Add the parser to the consumer-facing install path (for example a peer/optional peer plus docs) or remove the requirement.

Useful? React with 👍 / 👎.

"buildPlugin": {
"hideFromRootReadme": true
}
}
69 changes: 69 additions & 0 deletions packages/published/eslint-plugin-apps/rollup.config.mjs
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()],
},
];
49 changes: 49 additions & 0 deletions packages/published/eslint-plugin-apps/src/index.ts
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';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive plugin metadata version from package.json

This hard-coded version will not be updated by the repo's yarn version:all workflow, which only bumps package manifests, so the published plugin metadata becomes stale after the next release even though ESLint expects plugin meta.version to match the package. That can make --print-config misleading and can keep --cache from noticing plugin updates; derive it from package.json like the other published packages do.

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' },
},
],
},
],
});
Loading
Loading