@numbered/docs-to-context 0.1.4 → 0.3.0
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.
- package/README.md +71 -16
- package/docs/liquid/README.mdx +59 -0
- package/docs/liquid/a11y-alt-text.mdx +16 -0
- package/docs/liquid/a11y-semantic-html.mdx +19 -0
- package/docs/liquid/alpine-cleanup.mdx +18 -0
- package/docs/liquid/alpine-debounce.mdx +12 -0
- package/docs/liquid/alpine-defer-heavy.mdx +13 -0
- package/docs/liquid/alpine-separate-scopes.mdx +19 -0
- package/docs/liquid/alpine-static-data.mdx +16 -0
- package/docs/liquid/css-critical-inline.mdx +16 -0
- package/docs/liquid/css-prefer-tailwind.mdx +30 -0
- package/docs/liquid/liquid-assign-over-capture.mdx +17 -0
- package/docs/liquid/liquid-avoid-nested-loops.mdx +21 -0
- package/docs/liquid/liquid-break-continue.mdx +23 -0
- package/docs/liquid/liquid-cache-assigns.mdx +16 -0
- package/docs/liquid/liquid-elsif-chain.mdx +25 -0
- package/docs/liquid/liquid-filter-early.mdx +18 -0
- package/docs/liquid/liquid-limit-loops.mdx +17 -0
- package/docs/liquid/liquid-map-join.mdx +16 -0
- package/docs/liquid/liquid-render-over-include.mdx +13 -0
- package/docs/liquid/liquid-snippet-data.mdx +17 -0
- package/docs/liquid/liquid-whitespace-control.mdx +17 -0
- package/docs/liquid/loading-defer-scripts.mdx +13 -0
- package/docs/liquid/loading-lazy-images.mdx +31 -0
- package/docs/liquid/loading-preload-critical.mdx +24 -0
- package/docs/liquid/loading-responsive-images.mdx +23 -0
- package/docs/liquid/schema-blocks-over-settings.mdx +35 -0
- package/docs/liquid/schema-section-settings.mdx +36 -0
- package/docs/react/README.mdx +99 -0
- package/docs/react/advanced-handler-refs.mdx +15 -0
- package/docs/react/advanced-use-latest.mdx +21 -0
- package/docs/react/async-defer-await.mdx +19 -0
- package/docs/react/async-promise-all.mdx +17 -0
- package/docs/react/async-start-early.mdx +25 -0
- package/docs/react/async-suspense-boundaries.mdx +35 -0
- package/docs/react/bundle-barrel-imports.mdx +25 -0
- package/docs/react/bundle-conditional-loading.mdx +21 -0
- package/docs/react/bundle-defer-third-party.mdx +15 -0
- package/docs/react/bundle-dynamic-imports.mdx +15 -0
- package/docs/react/bundle-preload-intent.mdx +24 -0
- package/docs/react/client-event-listeners.mdx +45 -0
- package/docs/react/client-swr-dedup.mdx +40 -0
- package/docs/react/effect-derive-state.mdx +12 -0
- package/docs/react/effect-event-handlers.mdx +16 -0
- package/docs/react/effect-key-reset.mdx +11 -0
- package/docs/react/effect-no-chains.mdx +22 -0
- package/docs/react/effect-notify-parents.mdx +18 -0
- package/docs/react/effect-use-memo.mdx +17 -0
- package/docs/react/js-batch-css.mdx +23 -0
- package/docs/react/js-cache-property.mdx +17 -0
- package/docs/react/js-cache-storage.mdx +29 -0
- package/docs/react/js-combine-iterations.mdx +20 -0
- package/docs/react/js-early-return.mdx +24 -0
- package/docs/react/js-hoist-regexp.mdx +21 -0
- package/docs/react/js-index-maps.mdx +14 -0
- package/docs/react/js-length-check.mdx +12 -0
- package/docs/react/js-loop-min-max.mdx +14 -0
- package/docs/react/js-set-lookups.mdx +12 -0
- package/docs/react/js-tosorted.mdx +15 -0
- package/docs/react/render-activity.mdx +17 -0
- package/docs/react/render-conditional.mdx +11 -0
- package/docs/react/render-content-visibility.mdx +12 -0
- package/docs/react/render-cx-clsx.mdx +12 -0
- package/docs/react/render-hoist-jsx.mdx +16 -0
- package/docs/react/render-hydration-flicker.mdx +25 -0
- package/docs/react/render-svg-precision.mdx +17 -0
- package/docs/react/render-svg-wrapper.mdx +19 -0
- package/docs/react/rerender-defer-reads.mdx +24 -0
- package/docs/react/rerender-derived-state.mdx +18 -0
- package/docs/react/rerender-inline-objects.mdx +18 -0
- package/docs/react/rerender-isolate-state.mdx +36 -0
- package/docs/react/rerender-lazy-init.mdx +19 -0
- package/docs/react/rerender-memo-extract.mdx +26 -0
- package/docs/react/rerender-narrow-deps.mdx +26 -0
- package/docs/react/rerender-transitions.mdx +29 -0
- package/docs/react/rerender-use-client-down.mdx +34 -0
- package/docs/react/server-lru-cache.mdx +24 -0
- package/docs/react/server-parallel-fetching.mdx +24 -0
- package/docs/react/server-react-cache.mdx +16 -0
- package/docs/react/server-rsc-serialization.mdx +17 -0
- package/package.json +2 -1
- package/scripts/extract.ts +2 -2
- package/scripts/generate.ts +9 -2
- package/scripts/inject.ts +95 -28
package/scripts/extract.ts
CHANGED
|
@@ -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, '
|
|
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, '
|
|
343
|
+
const outDir = outputDir ?? join(root, '.context', 'components')
|
|
344
344
|
const snippetsDir = join(root, 'snippets')
|
|
345
345
|
|
|
346
346
|
if (!isDir(snippetsDir)) {
|
package/scripts/generate.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,7 @@ const REGENERATE_CMD = 'bunx @numbered/docs-to-context'
|
|
|
28
30
|
interface CoreDocs {
|
|
29
31
|
frontend: string[]
|
|
30
32
|
entities: string[]
|
|
33
|
+
bestPractices: string[]
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
@@ -45,11 +48,57 @@ function groupByDir(paths: string[]): [string, string[]][] {
|
|
|
45
48
|
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b))
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
// ── Platform → doc folder mapping ─────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const PLATFORM_DOC_FOLDER: Record<string, string> = {
|
|
54
|
+
nextjs: 'react',
|
|
55
|
+
shopify: 'liquid',
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Best practices copy ───────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function copyBestPractices(projectRoot: string, platform: string): string[] {
|
|
61
|
+
const folder = PLATFORM_DOC_FOLDER[platform]
|
|
62
|
+
if (!folder) return []
|
|
63
|
+
|
|
64
|
+
const bundledDir = resolve(import.meta.dir, '..', 'docs', folder)
|
|
65
|
+
if (!isDir(bundledDir)) return []
|
|
66
|
+
|
|
67
|
+
const destDir = join(projectRoot, CONTEXT_ROOT, 'best-practices')
|
|
68
|
+
mkdirSync(destDir, { recursive: true })
|
|
69
|
+
|
|
70
|
+
const files = walkDir(bundledDir, ['.md', '.mdx'])
|
|
71
|
+
const copied: string[] = []
|
|
72
|
+
|
|
73
|
+
// Ensure README is first so the index is always listed before patterns
|
|
74
|
+
files.sort((a, b) => {
|
|
75
|
+
const aName = a.slice(a.lastIndexOf('/') + 1)
|
|
76
|
+
const bName = b.slice(b.lastIndexOf('/') + 1)
|
|
77
|
+
const aReadme = aName.toLowerCase().startsWith('readme')
|
|
78
|
+
const bReadme = bName.toLowerCase().startsWith('readme')
|
|
79
|
+
if (aReadme && !bReadme) return -1
|
|
80
|
+
if (!aReadme && bReadme) return 1
|
|
81
|
+
return aName.localeCompare(bName)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
for (const src of files) {
|
|
85
|
+
const name = src.slice(src.lastIndexOf('/') + 1)
|
|
86
|
+
copyFileSync(src, join(destDir, name))
|
|
87
|
+
copied.push(`best-practices/${name}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (copied.length) {
|
|
91
|
+
log.success(`Copied ${copied.length} best practice doc(s)`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return copied
|
|
95
|
+
}
|
|
96
|
+
|
|
48
97
|
// ── Core docs discovery ────────────────────────────────────────────────
|
|
49
98
|
|
|
50
|
-
function discoverCoreDocs(projectRoot: string): CoreDocs {
|
|
99
|
+
function discoverCoreDocs(projectRoot: string, bestPractices: string[]): CoreDocs {
|
|
51
100
|
const docsDir = join(projectRoot, CORE_DOCS_ROOT)
|
|
52
|
-
const result: CoreDocs = { frontend: [], entities: [] }
|
|
101
|
+
const result: CoreDocs = { frontend: [], entities: [], bestPractices }
|
|
53
102
|
|
|
54
103
|
if (!isDir(docsDir)) return result
|
|
55
104
|
|
|
@@ -69,16 +118,17 @@ function discoverCoreDocs(projectRoot: string): CoreDocs {
|
|
|
69
118
|
|
|
70
119
|
// ── Block builder ──────────────────────────────────────────────────────
|
|
71
120
|
|
|
72
|
-
function buildComponentLines(
|
|
121
|
+
function buildComponentLines(names: string[]): string[] {
|
|
122
|
+
const root = `./${CONTEXT_ROOT}/components`
|
|
73
123
|
return [
|
|
74
|
-
`[Component Index]|root:
|
|
124
|
+
`[Component Index]|root: ${root}`,
|
|
75
125
|
'|IMPORTANT: Read component MDX before using any component',
|
|
76
126
|
`|components:{${names.join(',')}}`,
|
|
77
|
-
`|If
|
|
127
|
+
`|If ${root} is missing, run: ${REGENERATE_CMD}`,
|
|
78
128
|
]
|
|
79
129
|
}
|
|
80
130
|
|
|
81
|
-
function buildIndexBlock(indexPath: string,
|
|
131
|
+
function buildIndexBlock(indexPath: string, coreDocs: CoreDocs | null): string | null {
|
|
82
132
|
const indexContent = readFileSync(indexPath, 'utf-8').trim()
|
|
83
133
|
|
|
84
134
|
const names: string[] = []
|
|
@@ -91,20 +141,29 @@ function buildIndexBlock(indexPath: string, docsDirRel: string, coreDocs: CoreDo
|
|
|
91
141
|
if (names.length === 0) return null
|
|
92
142
|
|
|
93
143
|
const lines: string[] = [MARKER_START, '## Project Docs']
|
|
94
|
-
const
|
|
144
|
+
const docsRoot = `./${CORE_DOCS_ROOT}`
|
|
95
145
|
|
|
96
146
|
// Frontend
|
|
97
147
|
if (coreDocs?.frontend.length) {
|
|
98
|
-
lines.push(`|[Frontend]|root: ${
|
|
148
|
+
lines.push(`|[Frontend]|root: ${docsRoot}`)
|
|
99
149
|
lines.push(`|frontend:{${coreDocs.frontend.join(',')}}`)
|
|
100
150
|
}
|
|
101
151
|
|
|
152
|
+
// Best Practices
|
|
153
|
+
if (coreDocs?.bestPractices.length) {
|
|
154
|
+
const contextRoot = `./${CONTEXT_ROOT}`
|
|
155
|
+
const fileNames = coreDocs.bestPractices.map((p) => p.slice(p.lastIndexOf('/') + 1))
|
|
156
|
+
lines.push(`|[Best Practices]|root: ${contextRoot}`)
|
|
157
|
+
lines.push(`|best-practices:{${fileNames.join(',')}}`)
|
|
158
|
+
lines.push('|IMPORTANT: Read best-practices/README.mdx first — it indexes all patterns by category and impact')
|
|
159
|
+
}
|
|
160
|
+
|
|
102
161
|
// Components (always present)
|
|
103
|
-
lines.push(...buildComponentLines(
|
|
162
|
+
lines.push(...buildComponentLines(names))
|
|
104
163
|
|
|
105
164
|
// Entities (grouped by directory)
|
|
106
165
|
if (coreDocs?.entities.length) {
|
|
107
|
-
lines.push(`|[Entities]|root: ${
|
|
166
|
+
lines.push(`|[Entities]|root: ${docsRoot}`)
|
|
108
167
|
for (const [dirPath, files] of groupByDir(coreDocs.entities)) {
|
|
109
168
|
lines.push(`|${dirPath}:{${files.join(',')}}`)
|
|
110
169
|
}
|
|
@@ -124,11 +183,15 @@ function injectIntoClaudeMd(claudeMdPath: string, block: string) {
|
|
|
124
183
|
|
|
125
184
|
const content = readFileSync(claudeMdPath, 'utf-8')
|
|
126
185
|
|
|
127
|
-
// Single-pass:
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
186
|
+
// Single-pass: replacer callback tracks whether a match occurred
|
|
187
|
+
let matched = false
|
|
188
|
+
const replaced = content.replace(MARKER_PATTERN, () => {
|
|
189
|
+
matched = true
|
|
190
|
+
return block
|
|
191
|
+
})
|
|
192
|
+
const newContent = matched
|
|
193
|
+
? replaced
|
|
194
|
+
: content.trimEnd() + '\n\n' + block + '\n'
|
|
132
195
|
|
|
133
196
|
writeFileSync(claudeMdPath, newContent, 'utf-8')
|
|
134
197
|
log.success('Injected index into CLAUDE.md')
|
|
@@ -136,18 +199,18 @@ function injectIntoClaudeMd(claudeMdPath: string, block: string) {
|
|
|
136
199
|
|
|
137
200
|
// ── .gitignore ─────────────────────────────────────────────────────────
|
|
138
201
|
|
|
139
|
-
function ensureGitignore(projectRoot: string
|
|
202
|
+
function ensureGitignore(projectRoot: string) {
|
|
140
203
|
const gitignorePath = join(projectRoot, '.gitignore')
|
|
141
|
-
const entry =
|
|
204
|
+
const entry = `${CONTEXT_ROOT}/`
|
|
142
205
|
|
|
143
206
|
if (existsSync(gitignorePath)) {
|
|
144
207
|
const content = readFileSync(gitignorePath, 'utf-8')
|
|
145
|
-
if (content.includes(entry) || content.includes(
|
|
208
|
+
if (content.includes(entry) || content.includes(CONTEXT_ROOT)) return
|
|
146
209
|
|
|
147
210
|
const padded = content.endsWith('\n') ? content : content + '\n'
|
|
148
|
-
writeFileSync(gitignorePath, padded + `\n# Generated
|
|
211
|
+
writeFileSync(gitignorePath, padded + `\n# Generated project context\n${entry}\n`, 'utf-8')
|
|
149
212
|
} else {
|
|
150
|
-
writeFileSync(gitignorePath, `# Generated
|
|
213
|
+
writeFileSync(gitignorePath, `# Generated project context\n${entry}\n`, 'utf-8')
|
|
151
214
|
}
|
|
152
215
|
|
|
153
216
|
log.info(`Added ${entry} to .gitignore`)
|
|
@@ -160,11 +223,12 @@ export interface InjectOptions {
|
|
|
160
223
|
indexPath?: string
|
|
161
224
|
claudeMdPath?: string
|
|
162
225
|
docsDir?: string
|
|
226
|
+
platform?: 'nextjs' | 'shopify'
|
|
163
227
|
}
|
|
164
228
|
|
|
165
229
|
export function inject(opts: InjectOptions) {
|
|
166
230
|
const root = resolve(opts.projectRoot)
|
|
167
|
-
const docsDir = opts.docsDir ??
|
|
231
|
+
const docsDir = opts.docsDir ?? `${CONTEXT_ROOT}/components`
|
|
168
232
|
const indexPath = opts.indexPath ?? join(root, docsDir, 'INDEX')
|
|
169
233
|
const claudeMdPath = opts.claudeMdPath ?? join(root, 'CLAUDE.md')
|
|
170
234
|
|
|
@@ -174,16 +238,19 @@ export function inject(opts: InjectOptions) {
|
|
|
174
238
|
process.exit(1)
|
|
175
239
|
}
|
|
176
240
|
|
|
177
|
-
|
|
178
|
-
const
|
|
241
|
+
// Copy best practices if platform is known
|
|
242
|
+
const bestPractices = opts.platform ? copyBestPractices(root, opts.platform) : []
|
|
243
|
+
|
|
244
|
+
const coreDocs = discoverCoreDocs(root, bestPractices)
|
|
245
|
+
const total = coreDocs.frontend.length + coreDocs.entities.length + coreDocs.bestPractices.length
|
|
179
246
|
if (total) log.info(`Found ${total} core doc(s)`)
|
|
180
247
|
|
|
181
|
-
const block = buildIndexBlock(indexPath,
|
|
248
|
+
const block = buildIndexBlock(indexPath, total ? coreDocs : null)
|
|
182
249
|
if (!block) {
|
|
183
250
|
log.error('INDEX file is empty, nothing to inject.')
|
|
184
251
|
process.exit(1)
|
|
185
252
|
}
|
|
186
253
|
|
|
187
254
|
injectIntoClaudeMd(claudeMdPath, block)
|
|
188
|
-
ensureGitignore(root
|
|
255
|
+
ensureGitignore(root)
|
|
189
256
|
}
|