check-stray-components.test.mjs

Shared component check tests

Tier 3engineering

Test that a private component reused from a second route is caught, and that stories, tests, Next special files and package imports of the same name are left alone, so an edit to the checker cannot quietly move the line.

CHECK-STRAY-COMPONENTS.TEST.MJS COMPONENT CHECK REUSED COMPONENTS NEAR MISSES TESTS PASS
Add this to your project
Add "Shared component check tests" from ntent to this repo.

Fetch https://ntent.app/r/f/check-stray-components-test as plain text. Write it verbatim to scripts/check-stray-components.test.mjs.

It needs check-stray-components.mjs. If this repo does not have it, stop and tell me.

Then check it: node --test passes. Stage an edit to check-stray-components.mjs that stops a reused private component being caught and the commit is refused, naming this file.

Reads https://ntent.app/r/f/check-stray-components-test

Read the code
check-stray-components.test.mjsscripts/check-stray-components.test.mjs
258 lines
/**
 * Tests for the stray-component checker, run against real files on disk.
 *
 * Each case is a pair, because the field study's complaint about this check was
 * noise, not misses: 22% of the findings were false and 40% were not worth the
 * reader's time. So for every thing it must convict there is a near miss that
 * looked like it and must pass: a Storybook story that is not a component, a
 * Next special file that is not meant to be promoted, an import of the same
 * name from a package, a default import under a different local name.
 *
 * The installer's customise step may change privateDirs, sharedDir and
 * scanDirs, so every run is of a copy of the script with those three pinned, as
 * check-env's tests do. Nothing here touches the repository you are running
 * from.
 *
 * Run:  node --test scripts/check-stray-components.test.mjs
 */

import test, { after } from 'node:test'
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'

// Every temp dir this file makes. They are removed when the file's tests end, pass or fail.
const made = []
const tmp = (prefix) => {
  const dir = mkdtempSync(join(tmpdir(), prefix))
  made.push(dir)
  return dir
}
after(() => {
  for (const dir of made) rmSync(dir, { recursive: true, force: true })
})

const CHECK = join(dirname(fileURLToPath(import.meta.url)), 'check-stray-components.mjs')

const PINNED = {
  privateDirs: "['src/app', 'src/features']",
  sharedDir: "'src/components'",
  scanDirs: "['src']",
}

/** Write { 'src/a.tsx': source } into a fresh repo and run a pinned copy there. */
function check(tree, args = ['--check'], { env, edit = (t) => t } = {}) {
  const dir = tmp('stray-')
  for (const [path, source] of Object.entries(tree)) {
    mkdirSync(dirname(join(dir, path)), { recursive: true })
    writeFileSync(join(dir, path), source)
  }
  let text = readFileSync(CHECK, 'utf8')
  for (const [key, literal] of Object.entries(PINNED)) {
    const re = new RegExp(`^  ${key}: .*,$`, 'm')
    assert.match(text, re, `CONFIG.${key} is on one line, so a test can replace it`)
    text = text.replace(re, `  ${key}: ${literal},`)
  }
  const script = join(dir, 'check-stray-components.pinned.mjs')
  writeFileSync(script, edit(text))
  try {
    const out = execFileSync('node', [script, ...args], { cwd: dir, encoding: 'utf8', env: env ? { ...process.env, ...env } : process.env })
    return { code: 0, out }
  } catch (e) {
    return { code: e.status, out: (e.stdout ?? '') + (e.stderr ?? '') }
  }
}

const jsx = (name) => `export function ${name}() {\n  return <div />\n}\n`
const kinds = (run) => [...run.out.matchAll(/^ {2}(REUSED-BUT-PRIVATE|DEFINED-TWICE) {2}(\w+)/gm)].map((m) => `${m[1]} ${m[2]}`)

// ----------------------------------------------------- the two checks ---

test('a private component imported from two other places is convicted', () => {
  const run = check({
    'src/app/a/_components/Row.tsx': jsx('Row'),
    'src/app/b/page.tsx': "import { Row } from '../a/_components/Row'\nexport default () => <Row />\n",
    'src/app/c/page.tsx': "import { Row } from '../a/_components/Row'\nexport default () => <Row />\n",
  })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(kinds(run), ['REUSED-BUT-PRIVATE Row'])
})

test('the same name defined in two files is convicted', () => {
  const run = check({
    'src/app/a/_components/Row.tsx': jsx('Row'),
    'src/app/b/_components/Row.tsx': jsx('Row'),
  })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(kinds(run), ['DEFINED-TWICE Row'])
})

// ------------------------------------------------ importer matching ---

test('an import of the same name from a package is not an importer', () => {
  const run = check({
    'src/app/a/_components/Row.tsx': jsx('Row'),
    'src/app/b/page.tsx': "import { Row } from 'some-ui-lib'\nexport default () => <Row />\n",
    'src/app/c/page.tsx': "import { Row } from '@acme/ui'\nexport default () => <Row />\n",
  })
  assert.equal(run.code, 0, run.out)
})

test('an @/ alias import resolves to the file, so it counts', () => {
  const run = check({
    'src/app/a/_components/Row.tsx': jsx('Row'),
    'src/app/b/page.tsx': "import { Row } from '@/app/a/_components/Row'\nexport default () => <Row />\n",
    'src/app/c/page.tsx': "import { Row } from '@/app/a/_components/Row'\nexport default () => <Row />\n",
  })
  assert.deepEqual(kinds(run), ['REUSED-BUT-PRIVATE Row'])
})

test('a default import counts, whatever the importer calls it', () => {
  const run = check({
    'src/app/a/_components/Card.tsx': 'export default function Card() {\n  return <div />\n}\n',
    'src/app/b/page.tsx': "import Tile from '../a/_components/Card'\nexport default () => <Tile />\n",
    'src/app/c/page.tsx': "import Box from '../a/_components/Card'\nexport default () => <Box />\n",
  })
  assert.equal(run.code, 1, run.out)
  assert.deepEqual(kinds(run), ['REUSED-BUT-PRIVATE Card'])
})

test('a default import of a different file of the same name is not an importer', () => {
  const run = check({
    'src/app/a/_components/Card.tsx': 'export default function Card() {\n  return <div />\n}\n',
    'src/app/b/page.tsx': "import Card from './Card'\nexport default () => <Card />\n",
    'src/app/b/Card.tsx': 'export default function Card() {\n  return <span />\n}\n',
    'src/app/c/page.tsx': "import Card from './Card'\nexport default () => <Card />\n",
    'src/app/c/Card.tsx': jsx('Other'),
  })
  // Two Card definitions is its own finding. What must not appear is a REUSED
  // finding for the one in a/, which nothing imports.
  assert.ok(!kinds(run).includes('REUSED-BUT-PRIVATE Card'), run.out)
})

// ------------------------------------------ what is not a component ---

test('Storybook stories are not components, and importing from one is not reuse', () => {
  const run = check({
    'src/app/a/_components/Row.tsx': jsx('Row'),
    'src/app/a/_components/Row.stories.tsx':
      "import { Row } from './Row'\nexport const Primary = () => <Row />\nexport const Wide = () => <Row />\n",
    'src/app/b/_components/Row2.stories.tsx':
      "import { Row } from '../../a/_components/Row'\nexport const Primary = () => <Row />\n",
    'src/app/c/_components/Row3.stories.tsx':
      "import { Row } from '../../a/_components/Row'\nexport const Primary = () => <Row />\n",
  })
  assert.equal(run.code, 0, run.out)
})

test('Next special files are not promotable and do not count as defined twice', () => {
  const tree = {}
  for (const f of ['loading', 'error', 'not-found', 'layout', 'page', 'template', 'default']) {
    for (const route of ['a', 'b']) {
      tree[`src/app/${route}/${f}.tsx`] = `export default function ${f.replace(/(^|-)(\w)/g, (_, __, c) => c.toUpperCase())}() {\n  return <div />\n}\n`
    }
  }
  tree['src/app/a/route.ts'] = 'export async function GET() { return Response.json({}) }\n'
  const run = check(tree)
  assert.equal(run.code, 0, run.out)
})

// -------------------------------------------------------- hollow runs ---

test('zero files it can read is exit 2, with what it looked for and what to set', () => {
  const run = check({ 'src/main.ts': 'export {}\n', 'README.md': '# hi\n' }, ['--check'])
  assert.equal(run.code, 2, run.out)
  assert.doesNotMatch(run.out, /components: ok/)
  assert.match(run.out, /Read 0 \.tsx\/\.jsx files under src/)
  assert.match(run.out, /CONFIG\.scanDirs/)
})

test('a tree of .vue, .svelte and .astro files names them', () => {
  const run = check({
    'src/App.vue': '<template><div /></template>\n',
    'src/Card.svelte': '<div />\n',
    'src/pages/index.astro': '<div />\n',
  })
  assert.equal(run.code, 2, run.out)
  assert.match(run.out, /found 3 \.astro, \.svelte, \.vue file\(s\)/)
  assert.match(run.out, /does not read them/)
})

test('a scanDirs that does not exist says so', () => {
  const run = check({ 'lib/a.tsx': jsx('A') }, ['--check'])
  assert.equal(run.code, 2, run.out)
  assert.match(run.out, /src does not exist/)
})

// ------------------------------------------------------- staged paths ---

const DEBT = {
  'src/app/a/_components/Row.tsx': jsx('Row'),
  'src/app/b/page.tsx': "import { Row } from '../a/_components/Row'\nexport default () => <Row />\n",
  'src/app/c/page.tsx': "import { Row } from '../a/_components/Row'\nexport default () => <Row />\n",
  'src/app/d/page.tsx': 'export default () => <div />\n',
}

test('staged paths: debt in files nobody is committing is a count, not a block', () => {
  const run = check(DEBT, ['--check', 'src/app/d/page.tsx'])
  assert.equal(run.code, 0, run.out)
  assert.match(run.out, /1 finding\(s\) involve no staged file/)
  assert.doesNotMatch(kinds(run).join(), /Row/)
})

test('staged paths: a finding that involves a staged file still blocks', () => {
  for (const staged of ['src/app/a/_components/Row.tsx', 'src/app/b/page.tsx']) {
    const run = check(DEBT, ['--check', staged])
    assert.equal(run.code, 1, `${staged}\n${run.out}`)
    assert.deepEqual(kinds(run), ['REUSED-BUT-PRIVATE Row'])
  }
})

test('staged paths that are outside scope are exit 0 with a note, not a hollow pass and not exit 2', () => {
  const run = check(DEBT, ['--check', 'docs/notes.md'])
  assert.equal(run.code, 0, run.out)
  assert.match(run.out, /nothing staged is a file this reads/)
})

test('without paths, the whole repo still blocks', () => {
  assert.equal(check(DEBT).code, 1)
})

// ------------------------------------------------------ broken allowlist ---

const withAllowlist = (entries) => (text) => text.replace('const ALLOWLIST = {\n', `const ALLOWLIST = {\n${entries}\n`)

test('an allowlist entry with no reason, no date or a loose date is exit 1, so it blocks', () => {
  // Exit 2 is "read nothing", which the pre-commit hook makes a note. A broken
  // allowlist is the config the check exists to enforce, and it must refuse.
  for (const entry of [
    "Row: { until: '2099-01-01' },",
    "Row: { reason: 'because' },",
    "Row: { reason: 'because', until: 'soon' },",
  ]) {
    for (const args of [['--check'], []]) {
      const run = check(DEBT, args, { edit: withAllowlist(entry) })
      assert.equal(run.code, 1, `${entry} ${args}\n${run.out}`)
      assert.match(run.out, /Allowlist entry "Row"/)
    }
  }
})

test('nothing staged that it reads is exit 0 and says so in the file the hook names', () => {
  const dir = tmp('stray-note-')
  const note = join(dir, 'unchecked')
  writeFileSync(note, '')
  const run = check(DEBT, ['--check', 'docs/notes.md'], { env: { NTENT_UNCHECKED_FILE: note } })
  assert.equal(run.code, 0, run.out)
  assert.match(readFileSync(note, 'utf8'), /nothing staged is a file it reads/)
  // Without the variable nothing is written anywhere, and the run is as before.
  assert.equal(check(DEBT, ['--check', 'docs/notes.md']).code, 0)
  // And a run that did read a staged file leaves the file empty.
  writeFileSync(note, '')
  check(DEBT, ['--check', 'src/app/d/page.tsx'], { env: { NTENT_UNCHECKED_FILE: note } })
  assert.equal(readFileSync(note, 'utf8'), '')
})

How to tell it works: node --test passes. Stage an edit to check-stray-components.mjs that stops a reused private component being caught and the commit is refused, naming this file.

Included in