Sync /_lint with host's lint plugins and settings#4965
Conversation
There was a problem hiding this comment.
Pull request overview
Aligns the realm-server /_lint endpoint behavior with the host package’s actual lint configuration so server-side linting matches what CI (pnpm run lint) enforces, and adds template-lint coverage for .hbs/.gts/.gjs.
Changes:
- Replaced the hand-maintained in-task ESLint setup with dynamic loading of
packages/host/.eslintrc.jsand addedember-template-lintexecution (with per-messagesourcetagging). - Added engine caching and a pnpm
NODE_PATHshim to support plugin/parser resolution in a pnpm workspace layout. - Increased
/_lintjob timeout and added endpoint tests that prove host ESLint + template-lint rules are actually being applied.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| packages/runtime-common/tasks/lint.ts | Switches linting to use host ESLint + template-lint engines, adds caching/NODE_PATH shim, and normalizes result message shape. |
| packages/runtime-common/realm.ts | Increases the queued lint job timeout to accommodate heavier cold-start lint initialization. |
| packages/realm-server/tests/realm-endpoints/lint-test.ts | Adds regression tests that fail unless host ESLint config and ember-template-lint are both active. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Host Test Results 1 files ± 0 1 suites ±0 1h 20m 16s ⏱️ - 14m 57s Results for commit 4f205c9. ± Comparison against earlier commit 914c211. Realm Server Test Results 1 files ±0 1 suites ±0 9m 48s ⏱️ -4s Results for commit 4f205c9. ± Comparison against earlier commit 914c211. |
9379f4b to
914c211
Compare
| const passed = !messages.some((m) => m.severity === 2); | ||
| return { | ||
| passed, | ||
| output: working, | ||
| fixed: modified, | ||
| messages, | ||
| }; |
| // Input bounds | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; |
| const validatedFilename = validateFilename(filename); | ||
| const ext = extname(validatedFilename).toLowerCase(); | ||
| const messages: LintMessage[] = []; | ||
| let working = source; | ||
| let modified = false; | ||
|
|
||
| if (ESLINT_EXTENSIONS.has(ext)) { | ||
| const { output, messages: eslintMessages } = await runESLint( |
The realm-server's /_lint endpoint ran a hand-maintained ESLint config that
had drifted from what `pnpm run lint` runs in CI, and it didn't run
ember-template-lint at all. This matters for catalog submission lint (CS-10863):
pre-PR validation must agree with CI or submitters hit false greens/reds.
- Dynamically load host/.eslintrc.js (full plugin set, all 8 extends) instead
of the inline rule subset.
- Run ember-template-lint alongside ESLint, loading host/.template-lintrc.js.
- Tag each message with `source: 'eslint' | 'template-lint'`.
- Cache ESLint + TemplateLinter per worker process; cold ~1.2s, warm ~25-40ms.
- Anchor filePath at host package root (not under app/) so host's app-only
overrides like import/order don't apply to realm content.
- NODE_PATH shim so transitive parsers/plugins resolve under pnpm.
New tests prove three rules now fire that didn't before:
- ember/no-empty-glimmer-component-classes (host plugin:ember/recommended-gts)
- @cardstack/boxel/no-raf-for-state (host rule, missing from inline config)
- no-invalid-interactive (ember-template-lint, not previously run)
Endpoint shape unchanged: still POST /_lint with text body + X-Filename header,
still returns { output, fixed, messages }. The messages array can now contain
template-lint entries (tagged via the optional `source` field).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
914c211 to
4f205c9
Compare
Summary
/_lintendpoint ran a hand-maintained ESLint config that drifted from whatpnpm run lintruns in CI, and didn't runember-template-lintat all. This blocked catalog submission pre-PR lint (CS-10863) from matching CI's verdict.tasks/lint.tsnow loadspackages/host/.eslintrc.jsdynamically (full plugin set, all 8extends:) and runsember-template-lintagainstpackages/host/.template-lintrc.jsper file. Each message carries an optionalsource: 'eslint' | 'template-lint'tag.POST /_lintwith text body +X-Filenameheader, still returns{ output, fixed, messages }.Key implementation notes
ESLintandTemplateLinterinstances are constructed once per worker process. Cold start is ~1.2s (plugin tree resolution); warm calls are ~25-40ms.parser: 'ember-eslint-parser'in host's config) needs.pnpm/node_modulesonNODE_PATH, mirroring what pnpm's owneslintbin wrapper does.filePathis set to${HOST_PKG}/<filename>, not${HOST_PKG}/app/<filename>. Host'sapp/**-specific overrides (e.g.import/order) shouldn't apply to realm content; the broader**/*.gtsand**/*.{js,ts}overrides still match and bring in the full shared config.submission-lint.ts.New test coverage
Three new tests in
realm-endpoints/lint-test.tsverify rules that fire only if host's config is loaded:ember/no-empty-glimmer-component-classes(fromplugin:ember/recommended-gts)@cardstack/boxel/no-raf-for-state(host rule, missing from the legacy inline config)no-invalid-interactive(fromember-template-lint, which/_lintdid not previously invoke at all)Scope
This PR is the first of several under CS-10863:
/_lintconfig sync +ember-template-lintengine./_type_checkendpoint, re-enabling the bot-runner submission lint step, deletingruntime-common/lint/submission-lint.ts. Those land in follow-up tickets.Test plan
realm-endpoints/lint-test.tscases (18 tests) still pass against the new shape — they all usemessages/outputwhich is preserved.host/app/(currently passingpnpm run lint) return zero errors from the new/_lint.sourcefield on messages is additive.boxel lint <some-file.gts>against a local realm-server runs and reports findings.Linear: CS-11261
🤖 Generated with Claude Code