Skip to content

[APPS] Add @datadog/eslint-plugin-apps with valid-connections-file rule#332

Draft
sdkennedy2 wants to merge 2 commits into
sdkennedy2/apps-emit-connections-manifestfrom
sdkennedy2/eslint-plugin-apps
Draft

[APPS] Add @datadog/eslint-plugin-apps with valid-connections-file rule#332
sdkennedy2 wants to merge 2 commits into
sdkennedy2/apps-emit-connections-manifestfrom
sdkennedy2/eslint-plugin-apps

Conversation

@sdkennedy2
Copy link
Copy Markdown
Collaborator

@sdkennedy2 sdkennedy2 commented Apr 29, 2026

Motivation

Stacked on top of #331. That PR introduced the build-time extract-connections.ts extractor that fails the build when a project's connections.ts is malformed. PR #331 explicitly defers an ESLint rule for the same shape to a follow-up — this is that follow-up.

The rule mirrors the extractor's failure cases one-for-one so customers see violations in their editor instead of as a build error. Living in the same repo as the extractor means the two stay in lockstep — extractor contract changes land in the same PR as the rule that mirrors them.

Changes

New publishable npm package @datadog/eslint-plugin-apps at packages/published/eslint-plugin-apps/. Builds with rollup (custom config — getDefaultBuildConfigs is bundler-plugin specific and would mis-parse the package name), emits dual ESM (.mjs) + CJS (.js) + .d.ts to dist/src/. Versioned in lockstep with the rest of the @datadog/* packages (3.1.4) so yarn version:all keeps everything aligned.

Customers wire it up with either flat config or legacy .eslintrc:

// eslint.config.js (flat config, ESLint v9)
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 },
    },
];
// .eslintrc.json (legacy)
{
    "extends": ["plugin:@datadog/apps/recommended-legacy"],
    "parser": "@typescript-eslint/parser"
}

The package is structured to grow: more rules can be added under src/rules/<name>.ts and registered in src/index.ts.

Initial rule: valid-connections-file

Mirrors the extractor at packages/plugins/apps/src/backend/extract-connections.ts. Auto-scopes by basename — only runs on files matching connections.{ts,tsx,js,jsx}. Reports:

Violation Message ID
Missing top-level export const CONNECTIONS = {…} missingExport
Initializer not an object literal (after unwrapping as const etc.) notObjectLiteral
Two top-level export const CONNECTIONS duplicateExport
Spread element inside the object spreadElement
Computed key computedKey
Non-string-literal value valueNotStaticString
Template literal with interpolation templateInterpolation

Only CONNECTIONS (uppercase) is accepted, matching the extractor. The rule unwraps TSAsExpression / TSTypeAssertion / TSSatisfiesExpression / TSNonNullExpression before structural checks since @typescript-eslint/parser preserves those as AST nodes.

Notable build details

  • format: 'cjs' output, external: ['eslint', 'node:path', 'path'] — eslint plugins must be require()-loadable for legacy .eslintrc. Rollup's CJS emit for an export default source already produces module.exports = plugin directly, so no module.exports.default footer trick is needed (that workaround is only required when the bundler is esbuild).
  • Three rollup configs (ESM, CJS, DTS via rollup-plugin-dts).
  • buildPlugin.hideFromRootReadme: true keeps yarn cli integrity's root-README plugin table from gaining a row that doesn't fit (it's wired for bundler plugins).
  • The rule uses named @types/estree types internally for traversal (real type-checking on field access) and casts as unknown as Rule.Node only at the few context.report call sites. The reason for the casts: @types/eslint@9.6.1 ships a nested copy of @types/estree@1.0.7, while the rest of the repo (rollup, webpack, @dd/apps-plugin) pins 1.0.8. The two physical type modules are structurally identical but TypeScript treats them as distinct identities, so a VariableDeclarator from our top-level estree doesn't satisfy Rule.Node (which references @types/eslint's nested copy). Pinning our package to 1.0.7 doesn't help either — Node's module resolution still finds a different physical install path adjacent to our package vs. the one nested under @types/eslint. A small, localized cast at the report boundary is the cleanest fix; an asRuleNode helper documents the intent in one place.

QA Instructions

  1. yarn workspace @datadog/eslint-plugin-apps build — emits dist/src/index.{js,mjs,d.ts}.
  2. yarn workspace @dd/tests jest packages/published/eslint-plugin-apps/src/rules/valid-connections-file.test.ts — 13 RuleTester cases pass (4 valid + 9 invalid, including a "lowercase connections is not accepted" assertion).
  3. yarn lint, yarn typecheck:all — clean.
  4. yarn cli integrity — clean (no required updates).
  5. End-to-end against a scaffolded customer app:
    • yarn pack --out /tmp/eslint-plugin-apps.tgz from packages/published/eslint-plugin-apps/.
    • npm install --no-save /tmp/eslint-plugin-apps.tgz @typescript-eslint/parser eslint@9 in the test app.
    • Walk each violation case in connections.ts (missing export, lowercase connections, non-object init, duplicate, spread, computed key, identifier value, template interpolation). Each fires its corresponding messageId at the offending line:col.
    • CJS smoke test: node -e "console.log(Object.keys(require('@datadog/eslint-plugin-apps').rules))"[ 'valid-connections-file' ].

Blast Radius

  • Adds a new published-package directory; nothing in the existing build pipeline depends on it.
  • Not yet published to npm — until a release ships, no customer can install it.
  • New @datadog/* package picks up the existing version:all and release workflow automatically.
  • yarn cli integrity doesn't auto-add a row to the root README's plugin table thanks to buildPlugin.hideFromRootReadme: true. Future contributors browsing the table won't be misled into thinking this is a bundler plugin.

Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@sdkennedy2
Copy link
Copy Markdown
Collaborator Author

@codex review
@cursor review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d2f66f3a66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +65 to +67
"peerDependencies": {
"eslint": ">=8.57.0"
},
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 👍 / 👎.

Comment on lines +54 to +55
const basename = path.basename(context.filename);
if (!CONNECTIONS_BASENAME_RE.test(basename)) {
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 Scope the rule to the root connections file

Because the recommended config enables this rule globally, checking only path.basename(context.filename) makes any nested src/connections.ts or packages/foo/connections.js subject to the Apps root-file contract. The Vite plugin finder only reads connections.{ts,tsx,js,jsx} directly under buildRoot, so nested files are ignored at build time but now fail lint with missingExport or shape errors. Scope the recommended config/rule to the configured root file to avoid these false positives.

Useful? React with 👍 / 👎.

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 👍 / 👎.

@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/eslint-plugin-apps branch from 735769b to 9362c60 Compare April 30, 2026 13:53
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-emit-connections-manifest branch from 6f12a4e to b64302d Compare April 30, 2026 13:53
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/eslint-plugin-apps branch from 9362c60 to 0e68960 Compare May 4, 2026 14:46
@sdkennedy2
Copy link
Copy Markdown
Collaborator Author

@codex review
@cursor review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e68960f27

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

## 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant