@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.
- package/README.md +29 -12
- 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 +92 -25
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,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.
|
|
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(
|
|
123
|
+
function buildComponentLines(names: string[]): string[] {
|
|
124
|
+
const root = `./${CONTEXT_ROOT}/components`
|
|
73
125
|
return [
|
|
74
|
-
`[Component Index]|root:
|
|
126
|
+
`[Component Index]|root: ${root}`,
|
|
75
127
|
'|IMPORTANT: Read component MDX before using any component',
|
|
76
128
|
`|components:{${names.join(',')}}`,
|
|
77
|
-
`|If
|
|
129
|
+
`|If ${root} is missing, run: ${REGENERATE_CMD}`,
|
|
78
130
|
]
|
|
79
131
|
}
|
|
80
132
|
|
|
81
|
-
function buildIndexBlock(indexPath: string,
|
|
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
|
|
146
|
+
const docsRoot = `./${CORE_DOCS_ROOT}`
|
|
95
147
|
|
|
96
148
|
// Frontend
|
|
97
149
|
if (coreDocs?.frontend.length) {
|
|
98
|
-
lines.push(`|[Frontend]|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(
|
|
164
|
+
lines.push(...buildComponentLines(names))
|
|
104
165
|
|
|
105
166
|
// Entities (grouped by directory)
|
|
106
167
|
if (coreDocs?.entities.length) {
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
206
|
+
function ensureGitignore(projectRoot: string) {
|
|
144
207
|
const gitignorePath = join(projectRoot, '.gitignore')
|
|
145
|
-
const entry =
|
|
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(
|
|
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
|
|
215
|
+
writeFileSync(gitignorePath, padded + `\n# Generated project context\n${entry}\n`, 'utf-8')
|
|
153
216
|
} else {
|
|
154
|
-
writeFileSync(gitignorePath, `# Generated
|
|
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 ??
|
|
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
|
-
|
|
182
|
-
const
|
|
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,
|
|
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
|
|
259
|
+
ensureGitignore(root)
|
|
193
260
|
}
|