@numbered/docs-to-context 0.1.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +29 -12
  2. package/docs/liquid/README.mdx +59 -0
  3. package/docs/liquid/a11y-alt-text.mdx +16 -0
  4. package/docs/liquid/a11y-semantic-html.mdx +19 -0
  5. package/docs/liquid/alpine-cleanup.mdx +18 -0
  6. package/docs/liquid/alpine-debounce.mdx +12 -0
  7. package/docs/liquid/alpine-defer-heavy.mdx +13 -0
  8. package/docs/liquid/alpine-separate-scopes.mdx +19 -0
  9. package/docs/liquid/alpine-static-data.mdx +16 -0
  10. package/docs/liquid/css-critical-inline.mdx +16 -0
  11. package/docs/liquid/css-prefer-tailwind.mdx +30 -0
  12. package/docs/liquid/liquid-assign-over-capture.mdx +17 -0
  13. package/docs/liquid/liquid-avoid-nested-loops.mdx +21 -0
  14. package/docs/liquid/liquid-break-continue.mdx +23 -0
  15. package/docs/liquid/liquid-cache-assigns.mdx +16 -0
  16. package/docs/liquid/liquid-elsif-chain.mdx +25 -0
  17. package/docs/liquid/liquid-filter-early.mdx +18 -0
  18. package/docs/liquid/liquid-limit-loops.mdx +17 -0
  19. package/docs/liquid/liquid-map-join.mdx +16 -0
  20. package/docs/liquid/liquid-render-over-include.mdx +13 -0
  21. package/docs/liquid/liquid-snippet-data.mdx +17 -0
  22. package/docs/liquid/liquid-whitespace-control.mdx +17 -0
  23. package/docs/liquid/loading-defer-scripts.mdx +13 -0
  24. package/docs/liquid/loading-lazy-images.mdx +31 -0
  25. package/docs/liquid/loading-preload-critical.mdx +24 -0
  26. package/docs/liquid/loading-responsive-images.mdx +23 -0
  27. package/docs/liquid/schema-blocks-over-settings.mdx +35 -0
  28. package/docs/liquid/schema-section-settings.mdx +36 -0
  29. package/docs/react/README.mdx +99 -0
  30. package/docs/react/advanced-handler-refs.mdx +15 -0
  31. package/docs/react/advanced-use-latest.mdx +21 -0
  32. package/docs/react/async-defer-await.mdx +19 -0
  33. package/docs/react/async-promise-all.mdx +17 -0
  34. package/docs/react/async-start-early.mdx +25 -0
  35. package/docs/react/async-suspense-boundaries.mdx +35 -0
  36. package/docs/react/bundle-barrel-imports.mdx +25 -0
  37. package/docs/react/bundle-conditional-loading.mdx +21 -0
  38. package/docs/react/bundle-defer-third-party.mdx +15 -0
  39. package/docs/react/bundle-dynamic-imports.mdx +15 -0
  40. package/docs/react/bundle-preload-intent.mdx +24 -0
  41. package/docs/react/client-event-listeners.mdx +45 -0
  42. package/docs/react/client-swr-dedup.mdx +40 -0
  43. package/docs/react/effect-derive-state.mdx +12 -0
  44. package/docs/react/effect-event-handlers.mdx +16 -0
  45. package/docs/react/effect-key-reset.mdx +11 -0
  46. package/docs/react/effect-no-chains.mdx +22 -0
  47. package/docs/react/effect-notify-parents.mdx +18 -0
  48. package/docs/react/effect-use-memo.mdx +17 -0
  49. package/docs/react/js-batch-css.mdx +23 -0
  50. package/docs/react/js-cache-property.mdx +17 -0
  51. package/docs/react/js-cache-storage.mdx +29 -0
  52. package/docs/react/js-combine-iterations.mdx +20 -0
  53. package/docs/react/js-early-return.mdx +24 -0
  54. package/docs/react/js-hoist-regexp.mdx +21 -0
  55. package/docs/react/js-index-maps.mdx +14 -0
  56. package/docs/react/js-length-check.mdx +12 -0
  57. package/docs/react/js-loop-min-max.mdx +14 -0
  58. package/docs/react/js-set-lookups.mdx +12 -0
  59. package/docs/react/js-tosorted.mdx +15 -0
  60. package/docs/react/render-activity.mdx +17 -0
  61. package/docs/react/render-conditional.mdx +11 -0
  62. package/docs/react/render-content-visibility.mdx +12 -0
  63. package/docs/react/render-cx-clsx.mdx +12 -0
  64. package/docs/react/render-hoist-jsx.mdx +16 -0
  65. package/docs/react/render-hydration-flicker.mdx +25 -0
  66. package/docs/react/render-svg-precision.mdx +17 -0
  67. package/docs/react/render-svg-wrapper.mdx +19 -0
  68. package/docs/react/rerender-defer-reads.mdx +24 -0
  69. package/docs/react/rerender-derived-state.mdx +18 -0
  70. package/docs/react/rerender-inline-objects.mdx +18 -0
  71. package/docs/react/rerender-isolate-state.mdx +36 -0
  72. package/docs/react/rerender-lazy-init.mdx +19 -0
  73. package/docs/react/rerender-memo-extract.mdx +26 -0
  74. package/docs/react/rerender-narrow-deps.mdx +26 -0
  75. package/docs/react/rerender-transitions.mdx +29 -0
  76. package/docs/react/rerender-use-client-down.mdx +34 -0
  77. package/docs/react/server-lru-cache.mdx +24 -0
  78. package/docs/react/server-parallel-fetching.mdx +24 -0
  79. package/docs/react/server-react-cache.mdx +16 -0
  80. package/docs/react/server-rsc-serialization.mdx +17 -0
  81. package/package.json +2 -1
  82. package/scripts/extract.ts +2 -2
  83. package/scripts/generate.ts +9 -2
  84. package/scripts/inject.ts +92 -25
@@ -199,7 +199,7 @@ function generateNextjsMdx(comp: NextjsComponent): string {
199
199
 
200
200
  function scanNextjs(projectRoot: string, dirs?: string[], outputDir?: string): NextjsComponent[] {
201
201
  const root = resolve(projectRoot)
202
- const outDir = outputDir ?? join(root, 'docs', 'components')
202
+ const outDir = outputDir ?? join(root, '.context', 'components')
203
203
  const scanDirs = dirs ?? ['packages/ui/components']
204
204
 
205
205
  mkdirSync(outDir, { recursive: true })
@@ -340,7 +340,7 @@ function generateShopifyMdx(snippet: ShopifySnippet): string {
340
340
 
341
341
  function scanShopify(projectRoot: string, outputDir?: string): ShopifySnippet[] {
342
342
  const root = resolve(projectRoot)
343
- const outDir = outputDir ?? join(root, 'docs', 'components')
343
+ const outDir = outputDir ?? join(root, '.context', 'components')
344
344
  const snippetsDir = join(root, 'snippets')
345
345
 
346
346
  if (!isDir(snippetsDir)) {
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { resolve } from 'node:path'
14
- import { extract } from './extract'
14
+ import { detectPlatform, extract } from './extract'
15
15
  import { inject } from './inject'
16
16
  import { log } from './log'
17
17
 
@@ -42,6 +42,13 @@ for (let i = 0; i < args.length; i++) {
42
42
 
43
43
  projectRoot = resolve(projectRoot)
44
44
 
45
+ // ── Detect platform early ──────────────────────────────────────────────
46
+
47
+ if (!platform) {
48
+ const detected = detectPlatform(projectRoot)
49
+ if (detected) platform = detected
50
+ }
51
+
45
52
  // ── Run ────────────────────────────────────────────────────────────────
46
53
 
47
54
  log.banner()
@@ -50,6 +57,6 @@ log.section('Extract')
50
57
  extract({ projectRoot, platform, dirs, outputDir })
51
58
 
52
59
  log.section('Inject')
53
- inject({ projectRoot, docsDir: outputDir })
60
+ inject({ projectRoot, docsDir: outputDir, platform })
54
61
 
55
62
  log.done()
package/scripts/inject.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Inject project docs INDEX into CLAUDE.md between marker comments.
3
3
  *
4
- * Reads the INDEX file from docs/components/ and injects a compact reference
4
+ * Reads the INDEX file from .context/components/ and injects a compact reference
5
5
  * between <!-- PROJECT_DOCS_START --> and <!-- PROJECT_DOCS_END --> markers.
6
- * Also discovers core docs (design system, grid, architecture) and includes them.
6
+ * Also discovers core docs (design system, grid, architecture) and copies
7
+ * bundled best-practice docs for the detected platform.
7
8
  */
8
9
 
9
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
10
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
10
11
  import { join, relative, resolve } from 'node:path'
11
12
  import { isDir, isFile, walkDir } from './fs'
12
13
  import { log } from './log'
@@ -21,6 +22,7 @@ const MARKER_PATTERN = new RegExp(
21
22
  + MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
22
23
  )
23
24
  const CORE_DOCS_ROOT = 'docs'
25
+ const CONTEXT_ROOT = '.context'
24
26
  const REGENERATE_CMD = 'bunx @numbered/docs-to-context'
25
27
 
26
28
  // ── Types ──────────────────────────────────────────────────────────────
@@ -28,6 +30,8 @@ const REGENERATE_CMD = 'bunx @numbered/docs-to-context'
28
30
  interface CoreDocs {
29
31
  frontend: string[]
30
32
  entities: string[]
33
+ entitiesRoot: string
34
+ bestPractices: string[]
31
35
  }
32
36
 
33
37
  // ── Helpers ────────────────────────────────────────────────────────────
@@ -45,11 +49,57 @@ function groupByDir(paths: string[]): [string, string[]][] {
45
49
  return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))
46
50
  }
47
51
 
52
+ // ── Platform → doc folder mapping ─────────────────────────────────────
53
+
54
+ const PLATFORM_DOC_FOLDER: Record<string, string> = {
55
+ nextjs: 'react',
56
+ shopify: 'liquid',
57
+ }
58
+
59
+ // ── Best practices copy ───────────────────────────────────────────────
60
+
61
+ function copyBestPractices(projectRoot: string, platform: string): string[] {
62
+ const folder = PLATFORM_DOC_FOLDER[platform]
63
+ if (!folder) return []
64
+
65
+ const bundledDir = resolve(import.meta.dir, '..', 'docs', folder)
66
+ if (!isDir(bundledDir)) return []
67
+
68
+ const destDir = join(projectRoot, CONTEXT_ROOT, 'best-practices')
69
+ mkdirSync(destDir, { recursive: true })
70
+
71
+ const files = walkDir(bundledDir, ['.md', '.mdx'])
72
+ const copied: string[] = []
73
+
74
+ // Ensure README is first so the index is always listed before patterns
75
+ files.sort((a, b) => {
76
+ const aName = a.slice(a.lastIndexOf('/') + 1)
77
+ const bName = b.slice(b.lastIndexOf('/') + 1)
78
+ const aReadme = aName.toLowerCase().startsWith('readme')
79
+ const bReadme = bName.toLowerCase().startsWith('readme')
80
+ if (aReadme && !bReadme) return -1
81
+ if (!aReadme && bReadme) return 1
82
+ return aName.localeCompare(bName)
83
+ })
84
+
85
+ for (const src of files) {
86
+ const name = src.slice(src.lastIndexOf('/') + 1)
87
+ copyFileSync(src, join(destDir, name))
88
+ copied.push(`best-practices/${name}`)
89
+ }
90
+
91
+ if (copied.length) {
92
+ log.success(`Copied ${copied.length} best practice doc(s)`)
93
+ }
94
+
95
+ return copied
96
+ }
97
+
48
98
  // ── Core docs discovery ────────────────────────────────────────────────
49
99
 
50
- function discoverCoreDocs(projectRoot: string): CoreDocs {
100
+ function discoverCoreDocs(projectRoot: string, bestPractices: string[]): CoreDocs {
51
101
  const docsDir = join(projectRoot, CORE_DOCS_ROOT)
52
- const result: CoreDocs = { frontend: [], entities: [] }
102
+ const result: CoreDocs = { frontend: [], entities: [], entitiesRoot: '', bestPractices }
53
103
 
54
104
  if (!isDir(docsDir)) return result
55
105
 
@@ -61,7 +111,8 @@ function discoverCoreDocs(projectRoot: string): CoreDocs {
61
111
  // Entities: architecture specs
62
112
  const archDir = join(docsDir, 'specs', 'architecture')
63
113
  if (isDir(archDir)) {
64
- result.entities = walkDir(archDir, ['.md', '.mdx']).map((f) => relative(docsDir, f))
114
+ result.entitiesRoot = relative(docsDir, archDir)
115
+ result.entities = walkDir(archDir, ['.md', '.mdx']).map((f) => relative(archDir, f))
65
116
  }
66
117
 
67
118
  return result
@@ -69,16 +120,17 @@ function discoverCoreDocs(projectRoot: string): CoreDocs {
69
120
 
70
121
  // ── Block builder ──────────────────────────────────────────────────────
71
122
 
72
- function buildComponentLines(docsDirRel: string, names: string[]): string[] {
123
+ function buildComponentLines(names: string[]): string[] {
124
+ const root = `./${CONTEXT_ROOT}/components`
73
125
  return [
74
- `[Component Index]|root: ./${docsDirRel}`,
126
+ `[Component Index]|root: ${root}`,
75
127
  '|IMPORTANT: Read component MDX before using any component',
76
128
  `|components:{${names.join(',')}}`,
77
- `|If ./${docsDirRel} is missing, run: ${REGENERATE_CMD}`,
129
+ `|If ${root} is missing, run: ${REGENERATE_CMD}`,
78
130
  ]
79
131
  }
80
132
 
81
- function buildIndexBlock(indexPath: string, docsDirRel: string, coreDocs: CoreDocs | null): string | null {
133
+ function buildIndexBlock(indexPath: string, coreDocs: CoreDocs | null): string | null {
82
134
  const indexContent = readFileSync(indexPath, 'utf-8').trim()
83
135
 
84
136
  const names: string[] = []
@@ -91,22 +143,33 @@ function buildIndexBlock(indexPath: string, docsDirRel: string, coreDocs: CoreDo
91
143
  if (names.length === 0) return null
92
144
 
93
145
  const lines: string[] = [MARKER_START, '## Project Docs']
94
- const root = `./${CORE_DOCS_ROOT}`
146
+ const docsRoot = `./${CORE_DOCS_ROOT}`
95
147
 
96
148
  // Frontend
97
149
  if (coreDocs?.frontend.length) {
98
- lines.push(`|[Frontend]|root: ${root}`)
150
+ lines.push(`|[Frontend]|root: ${docsRoot}`)
99
151
  lines.push(`|frontend:{${coreDocs.frontend.join(',')}}`)
100
152
  }
101
153
 
154
+ // Best Practices
155
+ if (coreDocs?.bestPractices.length) {
156
+ const contextRoot = `./${CONTEXT_ROOT}`
157
+ const fileNames = coreDocs.bestPractices.map((p) => p.slice(p.lastIndexOf('/') + 1))
158
+ lines.push(`|[Best Practices]|root: ${contextRoot}`)
159
+ lines.push(`|best-practices:{${fileNames.join(',')}}`)
160
+ lines.push('|IMPORTANT: Read best-practices/README.mdx first — it indexes all patterns by category and impact')
161
+ }
162
+
102
163
  // Components (always present)
103
- lines.push(...buildComponentLines(docsDirRel, names))
164
+ lines.push(...buildComponentLines(names))
104
165
 
105
166
  // Entities (grouped by directory)
106
167
  if (coreDocs?.entities.length) {
107
- lines.push(`|[Entities]|root: ${root}`)
168
+ const entRoot = coreDocs.entitiesRoot ? `${docsRoot}/${coreDocs.entitiesRoot}` : docsRoot
169
+ lines.push(`|[Entities]|root: ${entRoot}`)
108
170
  for (const [dirPath, files] of groupByDir(coreDocs.entities)) {
109
- lines.push(`|${dirPath}:{${files.join(',')}}`)
171
+ const prefix = dirPath === '.' ? '' : `${dirPath}:`
172
+ lines.push(`|${prefix}{${files.join(',')}}`)
110
173
  }
111
174
  }
112
175
 
@@ -140,18 +203,18 @@ function injectIntoClaudeMd(claudeMdPath: string, block: string) {
140
203
 
141
204
  // ── .gitignore ─────────────────────────────────────────────────────────
142
205
 
143
- function ensureGitignore(projectRoot: string, docsDirRel: string) {
206
+ function ensureGitignore(projectRoot: string) {
144
207
  const gitignorePath = join(projectRoot, '.gitignore')
145
- const entry = docsDirRel.replace(/\/+$/, '') + '/'
208
+ const entry = `${CONTEXT_ROOT}/`
146
209
 
147
210
  if (existsSync(gitignorePath)) {
148
211
  const content = readFileSync(gitignorePath, 'utf-8')
149
- if (content.includes(entry) || content.includes(docsDirRel)) return
212
+ if (content.includes(entry) || content.includes(CONTEXT_ROOT)) return
150
213
 
151
214
  const padded = content.endsWith('\n') ? content : content + '\n'
152
- writeFileSync(gitignorePath, padded + `\n# Generated component docs\n${entry}\n`, 'utf-8')
215
+ writeFileSync(gitignorePath, padded + `\n# Generated project context\n${entry}\n`, 'utf-8')
153
216
  } else {
154
- writeFileSync(gitignorePath, `# Generated component docs\n${entry}\n`, 'utf-8')
217
+ writeFileSync(gitignorePath, `# Generated project context\n${entry}\n`, 'utf-8')
155
218
  }
156
219
 
157
220
  log.info(`Added ${entry} to .gitignore`)
@@ -164,11 +227,12 @@ export interface InjectOptions {
164
227
  indexPath?: string
165
228
  claudeMdPath?: string
166
229
  docsDir?: string
230
+ platform?: 'nextjs' | 'shopify'
167
231
  }
168
232
 
169
233
  export function inject(opts: InjectOptions) {
170
234
  const root = resolve(opts.projectRoot)
171
- const docsDir = opts.docsDir ?? 'docs/components'
235
+ const docsDir = opts.docsDir ?? `${CONTEXT_ROOT}/components`
172
236
  const indexPath = opts.indexPath ?? join(root, docsDir, 'INDEX')
173
237
  const claudeMdPath = opts.claudeMdPath ?? join(root, 'CLAUDE.md')
174
238
 
@@ -178,16 +242,19 @@ export function inject(opts: InjectOptions) {
178
242
  process.exit(1)
179
243
  }
180
244
 
181
- const coreDocs = discoverCoreDocs(root)
182
- const total = coreDocs.frontend.length + coreDocs.entities.length
245
+ // Copy best practices if platform is known
246
+ const bestPractices = opts.platform ? copyBestPractices(root, opts.platform) : []
247
+
248
+ const coreDocs = discoverCoreDocs(root, bestPractices)
249
+ const total = coreDocs.frontend.length + coreDocs.entities.length + coreDocs.bestPractices.length
183
250
  if (total) log.info(`Found ${total} core doc(s)`)
184
251
 
185
- const block = buildIndexBlock(indexPath, docsDir, total ? coreDocs : null)
252
+ const block = buildIndexBlock(indexPath, total ? coreDocs : null)
186
253
  if (!block) {
187
254
  log.error('INDEX file is empty, nothing to inject.')
188
255
  process.exit(1)
189
256
  }
190
257
 
191
258
  injectIntoClaudeMd(claudeMdPath, block)
192
- ensureGitignore(root, docsDir)
259
+ ensureGitignore(root)
193
260
  }