davaux 0.8.0 → 0.8.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 (90) hide show
  1. package/package.json +6 -2
  2. package/BASELINE.md +0 -169
  3. package/CLAUDE.md +0 -518
  4. package/ROADMAP.md +0 -198
  5. package/build.mjs +0 -101
  6. package/client/control.ts +0 -247
  7. package/client/hydrate.ts +0 -37
  8. package/client/index.ts +0 -19
  9. package/client/jsx-runtime.ts +0 -209
  10. package/client/resource.ts +0 -122
  11. package/client/signal.ts +0 -211
  12. package/client/store.ts +0 -110
  13. package/client/useHead.ts +0 -63
  14. package/pka.config.json +0 -32
  15. package/src/build/config.ts +0 -42
  16. package/src/build/index.ts +0 -6
  17. package/src/build/plugins.ts +0 -118
  18. package/src/cli.ts +0 -502
  19. package/src/config.ts +0 -197
  20. package/src/create-multisite.ts +0 -310
  21. package/src/create.ts +0 -194
  22. package/src/dev/blueprints.ts +0 -75
  23. package/src/dev/components.ts +0 -108
  24. package/src/dev/insert.ts +0 -221
  25. package/src/dev/remove.ts +0 -677
  26. package/src/dev/watch.ts +0 -3098
  27. package/src/errors.ts +0 -64
  28. package/src/generate.ts +0 -228
  29. package/src/index.ts +0 -67
  30. package/src/island.ts +0 -47
  31. package/src/jsx-runtime.d.ts +0 -408
  32. package/src/jsx-runtime.d.ts.map +0 -1
  33. package/src/jsx-runtime.ts +0 -536
  34. package/src/link.ts +0 -49
  35. package/src/oml/fragment.ts +0 -54
  36. package/src/oml/index.ts +0 -21
  37. package/src/oml/jsx-runtime.ts +0 -121
  38. package/src/oml/jsx.ts +0 -151
  39. package/src/oml/page.ts +0 -13
  40. package/src/oml/render.ts +0 -181
  41. package/src/oml/types.ts +0 -159
  42. package/src/router/handler.ts +0 -515
  43. package/src/router/matcher.ts +0 -52
  44. package/src/router/scanner.ts +0 -272
  45. package/src/server/index.ts +0 -49
  46. package/src/signal.ts +0 -39
  47. package/src/ssg.ts +0 -253
  48. package/src/test/actions.test.ts +0 -40
  49. package/src/test/body-limits.test.ts +0 -83
  50. package/src/test/errors.test.ts +0 -53
  51. package/src/test/fixtures/routes/[id].page.ts +0 -3
  52. package/src/test/fixtures/routes/_error.ts +0 -6
  53. package/src/test/fixtures/routes/_global.ts +0 -8
  54. package/src/test/fixtures/routes/_layout-template.ts +0 -7
  55. package/src/test/fixtures/routes/_layout.ts +0 -7
  56. package/src/test/fixtures/routes/_layout_scripts.ts +0 -8
  57. package/src/test/fixtures/routes/_middleware.ts +0 -8
  58. package/src/test/fixtures/routes/_redirect301_mw.ts +0 -5
  59. package/src/test/fixtures/routes/_redirect_mw.ts +0 -5
  60. package/src/test/fixtures/routes/about.page.ts +0 -6
  61. package/src/test/fixtures/routes/action.page.ts +0 -11
  62. package/src/test/fixtures/routes/api/form-all.post.ts +0 -5
  63. package/src/test/fixtures/routes/api/form-limited.post.ts +0 -6
  64. package/src/test/fixtures/routes/api/response-obj.get.ts +0 -17
  65. package/src/test/fixtures/routes/api/upload.post.ts +0 -14
  66. package/src/test/fixtures/routes/api/users.get.ts +0 -3
  67. package/src/test/fixtures/routes/api/xml.get.ts +0 -5
  68. package/src/test/fixtures/routes/auth/_middleware.ts +0 -11
  69. package/src/test/fixtures/routes/auth/protected.page.ts +0 -3
  70. package/src/test/fixtures/routes/index.page.ts +0 -3
  71. package/src/test/fixtures/routes/oml.page.ts +0 -7
  72. package/src/test/fixtures/routes/redirect.page.ts +0 -3
  73. package/src/test/fixtures/routes/ssg/[slug].page.ts +0 -8
  74. package/src/test/fixtures/routes/ssg/server.page.ts +0 -5
  75. package/src/test/fixtures/routes/state.page.ts +0 -4
  76. package/src/test/fixtures/routes/throw.page.ts +0 -5
  77. package/src/test/fixtures/routes/wiki/[...slug].page.ts +0 -3
  78. package/src/test/helpers.ts +0 -132
  79. package/src/test/layouts.test.ts +0 -76
  80. package/src/test/middleware.test.ts +0 -69
  81. package/src/test/multipart.test.ts +0 -91
  82. package/src/test/oml-routing.test.ts +0 -59
  83. package/src/test/oml.test.ts +0 -429
  84. package/src/test/redirects.test.ts +0 -32
  85. package/src/test/routing.test.ts +0 -118
  86. package/src/test/ssg.test.ts +0 -273
  87. package/src/test/web-response.test.ts +0 -33
  88. package/src/types.ts +0 -670
  89. package/tsconfig.client.json +0 -17
  90. package/tsconfig.json +0 -20
@@ -1,310 +0,0 @@
1
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
2
- import { join, resolve } from 'node:path'
3
-
4
- export async function scaffoldMultisite(name: string | undefined, cwd: string) {
5
- if (!name) {
6
- console.error('Usage: davaux create-multisite <project-name>')
7
- process.exit(1)
8
- }
9
-
10
- if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(name)) {
11
- console.error(
12
- `[davaux] Invalid project name "${name}". Use lowercase letters, numbers, hyphens, underscores, or dots.`,
13
- )
14
- process.exit(1)
15
- }
16
-
17
- const dir = resolve(cwd, name)
18
-
19
- if (existsSync(dir)) {
20
- console.error(`[davaux] Directory "${name}" already exists.`)
21
- process.exit(1)
22
- }
23
-
24
- const title = name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
25
-
26
- mkdirSync(join(dir, 'src', 'routes'), { recursive: true })
27
- mkdirSync(join(dir, 'sites', 'main', 'routes'), { recursive: true })
28
- mkdirSync(join(dir, 'public'), { recursive: true })
29
-
30
- write(dir, 'package.json', packageJson(name))
31
- write(dir, 'tsconfig.json', tsConfig())
32
- write(dir, 'biome.json', biomeConfig())
33
- write(dir, 'multisite.config.ts', multisiteConfig())
34
- write(dir, 'server.ts', serverEntry())
35
- write(dir, 'build.ts', buildEntry())
36
- write(dir, 'src/routes/_layout.tsx', baseLayout(title))
37
- write(dir, 'src/routes/index.page.tsx', baseIndexPage(title))
38
- write(dir, 'sites/main/routes/_layout.tsx', siteLayout())
39
- write(dir, 'sites/main/routes/index.page.tsx', siteIndexPage(title))
40
-
41
- console.log(`\n Created multisite project: ${name}\n`)
42
- console.log(' Next steps:\n')
43
- console.log(` cd ${name}`)
44
- console.log(' npm install')
45
- console.log(' npm run dev\n')
46
- console.log(' Docs: https://davaux.codeberg.page/docs/packages/multisite\n')
47
- }
48
-
49
- function write(dir: string, file: string, content: string) {
50
- writeFileSync(join(dir, file), content, 'utf8')
51
- }
52
-
53
- function packageJson(name: string) {
54
- return (
55
- JSON.stringify(
56
- {
57
- name,
58
- version: '0.1.0',
59
- type: 'module',
60
- scripts: {
61
- dev: 'node --watch --import tsx/esm server.ts',
62
- build: 'tsx build.ts',
63
- start: 'NODE_ENV=production node dist/server.js',
64
- },
65
- dependencies: {
66
- '@davaux/multisite': 'latest',
67
- davaux: 'latest',
68
- },
69
- devDependencies: {
70
- '@biomejs/biome': 'latest',
71
- '@types/node': 'latest',
72
- tsx: 'latest',
73
- },
74
- },
75
- null,
76
- 2,
77
- ) + '\n'
78
- )
79
- }
80
-
81
- function tsConfig() {
82
- return (
83
- JSON.stringify(
84
- {
85
- compilerOptions: {
86
- target: 'ESNext',
87
- module: 'NodeNext',
88
- moduleResolution: 'NodeNext',
89
- jsx: 'react-jsx',
90
- jsxImportSource: 'davaux',
91
- strict: true,
92
- lib: ['ESNext', 'DOM'],
93
- allowImportingTsExtensions: true,
94
- noEmit: true,
95
- skipLibCheck: true,
96
- types: ['node', 'davaux/env'],
97
- },
98
- include: ['src/**/*', 'sites/**/*', '*.ts'],
99
- },
100
- null,
101
- 2,
102
- ) + '\n'
103
- )
104
- }
105
-
106
- function biomeConfig() {
107
- return (
108
- JSON.stringify(
109
- {
110
- $schema: 'https://biomejs.dev/schemas/2.4.15/schema.json',
111
- assist: { actions: { source: { organizeImports: 'on' } } },
112
- linter: {
113
- enabled: true,
114
- rules: {
115
- recommended: true,
116
- correctness: {
117
- noUnusedVariables: 'error',
118
- noUnusedImports: 'error',
119
- },
120
- style: {
121
- noNonNullAssertion: 'warn',
122
- },
123
- },
124
- },
125
- formatter: {
126
- enabled: true,
127
- indentStyle: 'space',
128
- indentWidth: 2,
129
- lineWidth: 100,
130
- },
131
- javascript: {
132
- formatter: {
133
- quoteStyle: 'single',
134
- trailingCommas: 'all',
135
- semicolons: 'asNeeded',
136
- },
137
- },
138
- css: {
139
- formatter: {
140
- enabled: true,
141
- quoteStyle: 'single',
142
- },
143
- },
144
- files: {
145
- includes: ['src/**', 'sites/**', '*.ts'],
146
- },
147
- },
148
- null,
149
- 2,
150
- ) + '\n'
151
- )
152
- }
153
-
154
- function multisiteConfig() {
155
- return `import { join } from 'node:path'
156
- import { fileURLToPath } from 'node:url'
157
- import { defineSites } from '@davaux/multisite'
158
-
159
- const root = fileURLToPath(new URL('.', import.meta.url))
160
-
161
- export interface SiteConfig {
162
- name: string
163
- primaryColor: string
164
- }
165
-
166
- export const sites = defineSites<SiteConfig>({
167
- baseDir: join(root, 'src/routes'),
168
- sites: [
169
- {
170
- name: 'main',
171
- hostname: 'localhost',
172
- routesDir: join(root, 'sites/main/routes'),
173
- config: { name: 'Main Site', primaryColor: '#1d4ed8' },
174
- },
175
- {
176
- name: 'fallback',
177
- hostname: '*',
178
- config: { name: 'Fallback', primaryColor: '#374151' },
179
- },
180
- ],
181
- })
182
- `
183
- }
184
-
185
- function serverEntry() {
186
- return `import { startMultisite } from '@davaux/multisite'
187
- import { sites } from './multisite.config.js'
188
-
189
- startMultisite(sites, { port: 3000, hostname: 'localhost', cwd: import.meta.dirname })
190
- `
191
- }
192
-
193
- function buildEntry() {
194
- return `import { buildMultisite } from '@davaux/multisite/build'
195
- import { sites } from './multisite.config.js'
196
-
197
- await buildMultisite(sites, { cwd: import.meta.dirname })
198
- `
199
- }
200
-
201
- function baseLayout(title: string) {
202
- return `import { defineLayout } from 'davaux'
203
-
204
- export default defineLayout(({ children, ctx }) => (
205
- <html lang="en">
206
- <head>
207
- <meta charset="utf-8" />
208
- <meta name="viewport" content="width=device-width, initial-scale=1" />
209
- <title>{ctx.head.title ?? '${title}'}</title>
210
- {ctx.head.description && <meta name="description" content={ctx.head.description} />}
211
- {Object.entries(ctx.head.meta).map(([name, content]) => (
212
- <meta name={name} content={content} />
213
- ))}
214
- {ctx.head.stylesheets.map((href) => (
215
- <link rel="stylesheet" href={href} />
216
- ))}
217
- </head>
218
- <body>
219
- {children as unknown as string}
220
- {ctx.head.scripts.map((src) => (
221
- <script type="module" src={src}></script>
222
- ))}
223
- </body>
224
- </html>
225
- ))
226
- `
227
- }
228
-
229
- function baseIndexPage(title: string) {
230
- return `import { definePage } from 'davaux'
231
-
232
- export default definePage((ctx) => {
233
- ctx.head.title = '${title}'
234
- return (
235
- <main>
236
- <h1>Welcome to ${title}</h1>
237
- <p>
238
- This page is served from the shared base routes (<code>src/routes/</code>). It is visible
239
- at any hostname not explicitly registered in <code>multisite.config.ts</code> — in this
240
- project, that means any hostname other than <code>localhost</code>.
241
- </p>
242
- <h2>Get started</h2>
243
- <ul>
244
- <li>
245
- <a href="https://davaux.codeberg.page/docs/packages/multisite">@davaux/multisite</a>
246
- {' — '}concepts, API reference, and operating modes
247
- </li>
248
- <li>
249
- <a href="https://davaux.codeberg.page/docs/routing">Routing</a>
250
- {' — '}file-based routing, dynamic segments, layouts, and middlewares
251
- </li>
252
- <li>
253
- <a href="https://davaux.codeberg.page/docs/getting-started">Getting started</a>
254
- {' — '}core Davaux concepts
255
- </li>
256
- </ul>
257
- <h2>Project structure</h2>
258
- <ul>
259
- <li>
260
- <code>multisite.config.ts</code> — site definitions and per-site config
261
- </li>
262
- <li>
263
- <code>src/routes/</code> — shared base routes (you are here)
264
- </li>
265
- <li>
266
- <code>sites/main/routes/</code> — main site overrides (served at <code>localhost</code>)
267
- </li>
268
- <li>
269
- <code>server.ts</code> — starts the dev and production server
270
- </li>
271
- <li>
272
- <code>build.ts</code> — production build script
273
- </li>
274
- </ul>
275
- </main>
276
- )
277
- })
278
- `
279
- }
280
-
281
- function siteLayout() {
282
- return `export { default } from '../../../src/routes/_layout.js'
283
- `
284
- }
285
-
286
- function siteIndexPage(title: string) {
287
- return `import { definePage } from 'davaux'
288
- import { getSite } from '@davaux/multisite'
289
- import type { SiteConfig } from '../../../multisite.config.js'
290
-
291
- export default definePage((ctx) => {
292
- const site = getSite<SiteConfig>(ctx)
293
- ctx.head.title = site?.name ?? '${title}'
294
- return (
295
- <main>
296
- <h1>Welcome to {site?.name ?? '${title}'}</h1>
297
- <p>
298
- You're looking at the <strong>main</strong> site. Edit{' '}
299
- <code>sites/main/routes/index.page.tsx</code> to get started.
300
- </p>
301
- <p>
302
- Site config is defined in <code>multisite.config.ts</code> and accessed here via{' '}
303
- <code>getSite(ctx)</code>. Use it for per-site themes, database URLs, feature flags —
304
- anything that varies between sites.
305
- </p>
306
- </main>
307
- )
308
- })
309
- `
310
- }
package/src/create.ts DELETED
@@ -1,194 +0,0 @@
1
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
2
- import { join, resolve } from 'node:path'
3
-
4
- export async function scaffold(name: string | undefined, cwd: string) {
5
- if (!name) {
6
- console.error('Usage: davaux create <project-name>')
7
- process.exit(1)
8
- }
9
-
10
- if (!/^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/.test(name)) {
11
- console.error(
12
- `[davaux] Invalid project name "${name}". Use lowercase letters, numbers, hyphens, underscores, or dots.`,
13
- )
14
- process.exit(1)
15
- }
16
-
17
- const dir = resolve(cwd, name)
18
-
19
- if (existsSync(dir)) {
20
- console.error(`[davaux] Directory "${name}" already exists.`)
21
- process.exit(1)
22
- }
23
-
24
- const title = name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
25
-
26
- mkdirSync(join(dir, 'src', 'routes'), { recursive: true })
27
- mkdirSync(join(dir, 'public'), { recursive: true })
28
-
29
- write(dir, 'package.json', packageJson(name))
30
- write(dir, 'tsconfig.json', tsConfig())
31
- write(dir, 'davaux.config.ts', davauxConfig())
32
- write(dir, 'biome.json', biomeConfig())
33
- write(dir, 'src/routes/_layout.tsx', layout(title))
34
- write(dir, 'src/routes/index.page.tsx', indexPage(title))
35
-
36
- console.log(`\n Created project: ${name}\n`)
37
- console.log(' Next steps:\n')
38
- console.log(` cd ${name}`)
39
- console.log(' npm install')
40
- console.log(' npm run dev\n')
41
- }
42
-
43
- function write(dir: string, file: string, content: string) {
44
- writeFileSync(join(dir, file), content, 'utf8')
45
- }
46
-
47
- function packageJson(name: string) {
48
- return (
49
- JSON.stringify(
50
- {
51
- name,
52
- version: '0.1.0',
53
- type: 'module',
54
- scripts: {
55
- dev: 'davaux dev',
56
- build: 'davaux build',
57
- start: 'davaux start',
58
- },
59
- dependencies: {
60
- davaux: 'latest',
61
- },
62
- devDependencies: {
63
- '@biomejs/biome': 'latest',
64
- '@types/node': 'latest',
65
- },
66
- },
67
- null,
68
- 2,
69
- ) + '\n'
70
- )
71
- }
72
-
73
- function tsConfig() {
74
- return (
75
- JSON.stringify(
76
- {
77
- compilerOptions: {
78
- target: 'ESNext',
79
- module: 'NodeNext',
80
- moduleResolution: 'NodeNext',
81
- jsx: 'react-jsx',
82
- jsxImportSource: 'davaux',
83
- strict: true,
84
- lib: ['ESNext', 'DOM'],
85
- allowImportingTsExtensions: true,
86
- noEmit: true,
87
- skipLibCheck: true,
88
- types: ['node', 'davaux/env'],
89
- },
90
- include: ['src/**/*'],
91
- },
92
- null,
93
- 2,
94
- ) + '\n'
95
- )
96
- }
97
-
98
- function davauxConfig() {
99
- return `import { defineConfig } from 'davaux/config'
100
-
101
- export default defineConfig({
102
- server: {
103
- port: 3000,
104
- hostname: 'localhost',
105
- },
106
- })
107
- `
108
- }
109
-
110
- function biomeConfig() {
111
- return (
112
- JSON.stringify(
113
- {
114
- $schema: 'https://biomejs.dev/schemas/2.4.15/schema.json',
115
- assist: { actions: { source: { organizeImports: 'on' } } },
116
- linter: {
117
- enabled: true,
118
- rules: {
119
- recommended: true,
120
- correctness: {
121
- noUnusedVariables: 'error',
122
- noUnusedImports: 'error',
123
- },
124
- style: {
125
- noNonNullAssertion: 'warn',
126
- },
127
- },
128
- },
129
- formatter: {
130
- enabled: true,
131
- indentStyle: 'space',
132
- indentWidth: 2,
133
- lineWidth: 100,
134
- },
135
- javascript: {
136
- formatter: {
137
- quoteStyle: 'single',
138
- trailingCommas: 'all',
139
- semicolons: 'asNeeded',
140
- },
141
- },
142
- css: {
143
- formatter: {
144
- enabled: true,
145
- quoteStyle: 'single',
146
- },
147
- },
148
- files: {
149
- includes: ['src/**'],
150
- },
151
- },
152
- null,
153
- 2,
154
- ) + '\n'
155
- )
156
- }
157
-
158
- function layout(title: string) {
159
- return `import { defineLayout } from 'davaux'
160
-
161
- export default defineLayout(({ children, ctx }) => (
162
- <html lang="en">
163
- <head>
164
- <meta charset="utf-8" />
165
- <meta name="viewport" content="width=device-width, initial-scale=1" />
166
- <title>{ctx.head.title ?? '${title}'}</title>
167
- {ctx.head.description && <meta name="description" content={ctx.head.description} />}
168
- {Object.entries(ctx.head.meta).map(([name, content]) => (
169
- <meta name={name} content={content} />
170
- ))}
171
- {ctx.head.stylesheets.map((href) => (
172
- <link rel="stylesheet" href={href} />
173
- ))}
174
- </head>
175
- <body>
176
- {children as unknown as string}
177
- {ctx.head.scripts.map((src) => (
178
- <script type="module" src={src}></script>
179
- ))}
180
- </body>
181
- </html>
182
- ))
183
- `
184
- }
185
-
186
- function indexPage(title: string) {
187
- return `import { definePage } from 'davaux'
188
-
189
- export default definePage((ctx) => {
190
- ctx.head.title = '${title}'
191
- return <h1>Welcome to ${title}</h1>
192
- })
193
- `
194
- }
@@ -1,75 +0,0 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
2
- import { extname, join } from 'node:path'
3
- import { renderToHtml } from '../oml/render.js'
4
- import type { OmlBlueprint, OmlPropSchema, OmlPropType } from '../oml/types.js'
5
- import { parseOmlBlueprint } from '../oml/types.js'
6
-
7
- export type BlueprintEntry = {
8
- id: string
9
- name: string
10
- props: Record<string, OmlPropSchema>
11
- previewHtml: string
12
- jsxSnippet: string
13
- imports: Record<string, string>
14
- }
15
-
16
- function collectJsonFiles(dir: string, out: string[]): void {
17
- if (!existsSync(dir)) return
18
- for (const entry of readdirSync(dir)) {
19
- const full = join(dir, entry)
20
- if (statSync(full).isDirectory()) {
21
- collectJsonFiles(full, out)
22
- } else if (extname(entry) === '.json') {
23
- out.push(full)
24
- }
25
- }
26
- }
27
-
28
- function propPlaceholder(schema: OmlPropSchema): string {
29
- if (schema.default !== undefined) {
30
- const d = schema.default
31
- if (typeof d === 'string') return `"${d}"`
32
- return `{${JSON.stringify(d)}}`
33
- }
34
- const placeholders: Record<OmlPropType, string> = {
35
- string: '"example"',
36
- number: '{0}',
37
- boolean: '{false}',
38
- function: '{() => {}}',
39
- node: '"content"',
40
- array: '{[]}',
41
- }
42
- return placeholders[schema.type] ?? '"example"'
43
- }
44
-
45
- function toUsageJsx(bp: OmlBlueprint): string {
46
- const entries = Object.entries(bp.props)
47
- if (entries.length === 0) return `<${bp.name} />`
48
- const lines = entries.map(([name, schema]) => ` ${name}=${propPlaceholder(schema)}`)
49
- return `<${bp.name}\n${lines.join('\n')}\n/>`
50
- }
51
-
52
- export function scanBlueprints(cwd: string): BlueprintEntry[] {
53
- const files: string[] = []
54
- collectJsonFiles(join(cwd, 'src', 'blueprints'), files)
55
- collectJsonFiles(join(cwd, 'src', 'components'), files)
56
-
57
- const results: BlueprintEntry[] = []
58
- for (const filePath of files) {
59
- try {
60
- const raw = JSON.parse(readFileSync(filePath, 'utf-8'))
61
- const bp = parseOmlBlueprint(raw)
62
- results.push({
63
- id: bp.id,
64
- name: bp.name,
65
- props: bp.props,
66
- previewHtml: bp.output ? renderToHtml(bp.output) : '',
67
- jsxSnippet: toUsageJsx(bp),
68
- imports: bp.imports ?? {},
69
- })
70
- } catch {
71
- // Skip files that aren't valid blueprints
72
- }
73
- }
74
- return results
75
- }
@@ -1,108 +0,0 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
2
- import { basename, extname, join, relative } from 'node:path'
3
- import type { OmlPropSchema, OmlPropType } from '../oml/types.js'
4
-
5
- export type ComponentEntry = {
6
- name: string
7
- file: string
8
- importPath: string
9
- props: Record<string, OmlPropSchema>
10
- hasBlueprintAlready: boolean
11
- }
12
-
13
- function mapTsType(rawType: string): OmlPropType {
14
- const t = rawType
15
- .trim()
16
- .replace(/\s*[|&]\s*(undefined|null)$/g, '')
17
- .trim()
18
- if (t === 'string') return 'string'
19
- if (t === 'number') return 'number'
20
- if (t === 'boolean') return 'boolean'
21
- if (/ReactNode|JSX\.Element|React\.ReactNode|OmlNode|OmlChild|VNode/.test(t)) return 'node'
22
- if (/Array<|[[\]]/.test(t)) return 'array'
23
- if (/=>/.test(t) || t === 'Function' || t.startsWith('(')) return 'function'
24
- return 'string'
25
- }
26
-
27
- function extractProps(source: string): Record<string, OmlPropSchema> {
28
- const props: Record<string, OmlPropSchema> = {}
29
- const m = source.match(/(?:interface\s+Props|type\s+Props\s*=)\s*\{([\s\S]*?)\}/)
30
- if (!m) return props
31
- for (const line of m[1].split('\n')) {
32
- const pm = line.match(/^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)(\?)?\s*:\s*([^;,\n]+)/)
33
- if (!pm) continue
34
- const name = pm[1]
35
- if (name === 'children') continue
36
- props[name] = { type: mapTsType(pm[3].trim()), required: pm[2] !== '?' }
37
- }
38
- return props
39
- }
40
-
41
- function collectComponentFiles(dir: string, out: string[]): void {
42
- if (!existsSync(dir)) return
43
- for (const entry of readdirSync(dir)) {
44
- const full = join(dir, entry)
45
- if (statSync(full).isDirectory()) {
46
- collectComponentFiles(full, out)
47
- } else if (['.tsx', '.ts', '.jsx', '.js'].includes(extname(entry)) && /^[A-Z]/.test(entry)) {
48
- out.push(full)
49
- }
50
- }
51
- }
52
-
53
- export function scanComponents(dir: string, cwd: string, blueprintsDir: string): ComponentEntry[] {
54
- const files: string[] = []
55
- collectComponentFiles(dir, files)
56
- const routesDir = join(cwd, 'src', 'routes')
57
- return files.map((filePath) => {
58
- const name = basename(filePath, extname(filePath))
59
- let props: Record<string, OmlPropSchema> = {}
60
- try {
61
- props = extractProps(readFileSync(filePath, 'utf-8'))
62
- } catch {}
63
- const noExt = filePath.replace(/\.(tsx?|jsx?)$/, '')
64
- const rel = relative(routesDir, noExt)
65
- const importPath = rel.startsWith('.') ? rel : `../${rel}`
66
- return {
67
- name,
68
- file: relative(cwd, filePath),
69
- importPath,
70
- props,
71
- hasBlueprintAlready: existsSync(join(blueprintsDir, `${name}.oml.json`)),
72
- }
73
- })
74
- }
75
-
76
- export function updateBlueprint(
77
- id: string,
78
- name: string,
79
- props: Record<string, OmlPropSchema>,
80
- blueprintsDir: string,
81
- ): { saved: boolean; error?: string } {
82
- const filePath = join(blueprintsDir, `${id}.oml.json`)
83
- try {
84
- let existing: Record<string, unknown> = {}
85
- if (existsSync(filePath)) existing = JSON.parse(readFileSync(filePath, 'utf-8'))
86
- writeFileSync(filePath, JSON.stringify({ ...existing, id, name, props }, null, 2), 'utf-8')
87
- return { saved: true }
88
- } catch (e) {
89
- return { saved: false, error: String(e) }
90
- }
91
- }
92
-
93
- export function saveBlueprint(
94
- name: string,
95
- props: Record<string, OmlPropSchema>,
96
- imports: Record<string, string>,
97
- blueprintsDir: string,
98
- ): { saved: boolean; file?: string; error?: string } {
99
- try {
100
- if (!existsSync(blueprintsDir)) mkdirSync(blueprintsDir, { recursive: true })
101
- const bp = { id: name, name, props, return: null, imports }
102
- const filePath = join(blueprintsDir, `${name}.oml.json`)
103
- writeFileSync(filePath, JSON.stringify(bp, null, 2), 'utf-8')
104
- return { saved: true, file: filePath }
105
- } catch (e) {
106
- return { saved: false, error: String(e) }
107
- }
108
- }