# NOTE: This rule flags calls to `dynamic` or `nextDynamic` (next/dynamic) that use a dynamic import.
# It cannot verify the import source — it matches by function name.
# If you have other functions named `dynamic` or `nextDynamic`, add a suppression comment.
id: no-dynamic-server-components
language: Tsx
severity: error
message: "dynamic()/nextDynamic() may wrap a server component — ensure the target has 'use client'"
files:
  - web/**/*.ts
  - web/**/*.tsx
ignores:
  - '**/*.test.ts'
  - '**/*.spec.ts'
  - '**/*.test.tsx'
  - '**/*.spec.tsx'
rule:
  any:
    - pattern: '$FN(() => import($PATH))'
    - pattern: '$FN(() => { return import($PATH); })'
    - pattern: '$FN(() => import($PATH).then($$$))'
    - pattern: '$FN(() => import($PATH), $OPTS)'
    - pattern: '$FN(() => { return import($PATH); }, $OPTS)'
    - pattern: '$FN(() => import($PATH).then($$$), $OPTS)'
constraints:
  FN:
    regex: '^(dynamic|nextDynamic)$'
  PATH:
    regex: "^['\"`](\\.\\./|\\./|@/)"
  # NOTE: `$OPTS` is only bound by the two-argument pattern arms above. This
  # `not` constraint filters out `{ ssr: false }` only for those arms; the
  # single-argument dynamic import arms still match and remain violations.
  OPTS:
    not:
      regex: '(?:\bssr\b|[''"]ssr[''"])\s*:\s*false\b'
examples:
  - code: "const Comp = dynamic(() => import('./my-component'))"
    isValid: false
    file: 'web/app/page.ts'
  - code: 'const Comp = dynamic(() => import(`./my-component`))'
    isValid: false
    file: 'web/app/page.ts'
  - code: "const Comp = dynamic(() => import('../components/comp'))"
    isValid: false
    file: 'web/app/page.ts'
  - code: "const Comp = dynamic(() => import('./add-tag-form').then(m => ({ default: m.AddTagForm })))"
    isValid: false
    file: 'web/app/page.ts'
  - code: "const Comp = nextDynamic(() => import('./my-component'))"
    isValid: false
    file: 'web/app/page.ts'
  - code: "const Comp = dynamic(() => import('./c'), { loading: () => null })"
    isValid: false
    file: 'web/app/page.ts'
  - code: "const Comp = dynamic(() => import('./c'), { not_ssr: false })"
    isValid: false
    file: 'web/app/page.ts'
  - code: "const Comp = dynamic(() => import('./c'), { ssr: falsey })"
    isValid: false
    file: 'web/app/page.ts'
  - code: "const Comp = dynamic(() => import('./c'), { ssr: false })"
    isValid: true
    file: 'web/app/page.ts'
  - code: 'const Comp = dynamic(() => import("./c"), { "ssr": false })'
    isValid: true
    file: 'web/app/page.ts'
  - code: "const Comp = dynamic(() => import('./c'), { 'ssr': false })"
    isValid: true
    file: 'web/app/page.ts'
  - code: "const Comp = lazy(() => import('./my-component'))"
    isValid: true
    file: 'web/app/page.ts'
  - code: const Comp = dynamic(() => import('./my-component'))
    isValid: false
    file: web/app/page.tsx
  - code: const Comp = dynamic(() => import(`./my-component`))
    isValid: false
    file: web/app/page.tsx
  - code: const Comp = dynamic(() => import('../components/comp'))
    isValid: false
    file: web/app/page.tsx
  - code: "const Comp = dynamic(() => import('./add-tag-form').then(m => ({ default: m.AddTagForm })))"
    isValid: false
    file: web/app/page.tsx
  - code: const Comp = nextDynamic(() => import('./my-component'))
    isValid: false
    file: web/app/page.tsx
  - code: "const Comp = dynamic(() => import('./c'), { loading: () => null })"
    isValid: false
    file: web/app/page.tsx
  - code: "const Comp = dynamic(() => import('./c'), { not_ssr: false })"
    isValid: false
    file: web/app/page.tsx
  - code: "const Comp = dynamic(() => import('./c'), { ssr: falsey })"
    isValid: false
    file: web/app/page.tsx
  - code: "const Comp = dynamic(() => import('./c'), { ssr: false })"
    isValid: true
    file: web/app/page.tsx
  - code: 'const Comp = dynamic(() => import("./c"), { "ssr": false })'
    isValid: true
    file: web/app/page.tsx
  - code: "const Comp = dynamic(() => import('./c'), { 'ssr': false })"
    isValid: true
    file: web/app/page.tsx
  - code: const Comp = lazy(() => import('./my-component'))
    isValid: true
    file: web/app/page.tsx
