@weaverclub/render 0.0.2 → 0.0.4
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/dist/entrypoint +0 -0
- package/package.json +6 -2
- package/.github/workflows/publish.yml +0 -44
- package/biome.json +0 -42
- package/build.ts +0 -62
- package/bun.lock +0 -733
- package/bunfig.toml +0 -3
- package/ideas.md +0 -11
- package/publish.ts +0 -62
- package/src/cli/command/renderCommand.ts +0 -112
- package/src/cli/entrypoint.ts +0 -25
- package/src/core/bundler.ts +0 -180
- package/src/core/control/arrayControl.ts +0 -15
- package/src/core/control/booleanControl.ts +0 -15
- package/src/core/control/control.ts +0 -7
- package/src/core/control/controlBuilder.ts +0 -87
- package/src/core/control/numberControl.ts +0 -15
- package/src/core/control/stringControl.ts +0 -15
- package/src/core/control/variantControl.ts +0 -18
- package/src/core/css/css.ts +0 -50
- package/src/core/css/tailwind.ts +0 -172
- package/src/core/html.ts +0 -63
- package/src/core/pkg.ts +0 -92
- package/src/core/story.ts +0 -52
- package/src/core/tsconfig.ts +0 -46
- package/src/react/react.ts +0 -2
- package/src/react/reactControlBuilder.ts +0 -130
- package/src/react/reactStory.ts +0 -36
- package/src/server/api/getStories.ts +0 -44
- package/src/server/api/renderIframe.ts +0 -66
- package/src/server/backend.ts +0 -104
- package/src/server/streaming.ts +0 -16
- package/src/ui/api.ts +0 -16
- package/src/ui/app.tsx +0 -23
- package/src/ui/cn.ts +0 -6
- package/src/ui/components/appSidebar.tsx +0 -76
- package/src/ui/components/button.stories.tsx +0 -32
- package/src/ui/components/button.tsx +0 -55
- package/src/ui/components/command.tsx +0 -187
- package/src/ui/components/contextMenu.tsx +0 -261
- package/src/ui/components/dialog.tsx +0 -153
- package/src/ui/components/input.tsx +0 -23
- package/src/ui/components/inputGroup.tsx +0 -157
- package/src/ui/components/kdb.tsx +0 -26
- package/src/ui/components/searchCommand.tsx +0 -5
- package/src/ui/components/separator.tsx +0 -22
- package/src/ui/components/sheet.tsx +0 -131
- package/src/ui/components/sidebar.tsx +0 -725
- package/src/ui/components/skeleton.tsx +0 -13
- package/src/ui/components/spinner.tsx +0 -15
- package/src/ui/components/tabButton.tsx +0 -80
- package/src/ui/components/tabContent.tsx +0 -20
- package/src/ui/components/tabList.tsx +0 -53
- package/src/ui/components/textarea.tsx +0 -17
- package/src/ui/components/tooltip.tsx +0 -67
- package/src/ui/frontend.tsx +0 -68
- package/src/ui/hooks/useMobile.ts +0 -23
- package/src/ui/index.html +0 -12
- package/src/ui/routeTree.gen.ts +0 -35
- package/src/ui/routes/__root.tsx +0 -9
- package/src/ui/styles.css +0 -123
- package/src/ui/tabs.tsx +0 -89
- package/tsconfig.json +0 -25
- package/tsr.config.json +0 -6
package/src/core/css/tailwind.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { Array as Arr, Effect, Option } from 'effect'
|
|
2
|
-
|
|
3
|
-
// Path to the CLI bundled with this package (relative to this file in src/core/css/)
|
|
4
|
-
const BUNDLED_CLI = new URL(
|
|
5
|
-
'../../../node_modules/@tailwindcss/cli/dist/index.mjs',
|
|
6
|
-
import.meta.url
|
|
7
|
-
).pathname
|
|
8
|
-
|
|
9
|
-
// Cache for compiled CSS output - always kept updated
|
|
10
|
-
let cachedCssOutput: string | null = null
|
|
11
|
-
let tailwindWatcher: {
|
|
12
|
-
proc: ReturnType<typeof Bun.spawn>
|
|
13
|
-
outputFile: string
|
|
14
|
-
} | null = null
|
|
15
|
-
|
|
16
|
-
export const compileTailwindCss = Effect.fn(function* (projectRoot: string) {
|
|
17
|
-
const glob = new Bun.Glob('**/*.css')
|
|
18
|
-
|
|
19
|
-
const files = Arr.fromIterable(
|
|
20
|
-
glob.scanSync({ absolute: true, cwd: projectRoot })
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
const entrypoint = Arr.head(files)
|
|
24
|
-
|
|
25
|
-
if (Option.isNone(entrypoint))
|
|
26
|
-
return yield* Effect.fail(new Error('No CSS files found for Tailwind CSS.'))
|
|
27
|
-
|
|
28
|
-
// Use bundled @tailwindcss/cli - runs via bun for speed
|
|
29
|
-
const proc = Bun.spawn(
|
|
30
|
-
['bun', BUNDLED_CLI, '-i', entrypoint.value, '--minify'],
|
|
31
|
-
{
|
|
32
|
-
cwd: projectRoot,
|
|
33
|
-
stdout: 'pipe',
|
|
34
|
-
stderr: 'pipe'
|
|
35
|
-
}
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
const [stdout, stderr] = yield* Effect.all(
|
|
39
|
-
[
|
|
40
|
-
Effect.tryPromise(() => new Response(proc.stdout).text()),
|
|
41
|
-
Effect.tryPromise(() => new Response(proc.stderr).text())
|
|
42
|
-
],
|
|
43
|
-
{
|
|
44
|
-
concurrency: 'unbounded'
|
|
45
|
-
}
|
|
46
|
-
)
|
|
47
|
-
const exitCode = yield* Effect.tryPromise(() => proc.exited)
|
|
48
|
-
|
|
49
|
-
if (exitCode !== 0)
|
|
50
|
-
return yield* Effect.fail(
|
|
51
|
-
new Error(`Tailwind CSS compilation failed: ${stderr}`)
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
cachedCssOutput = stdout
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
paths: files,
|
|
58
|
-
compiledOutput: stdout
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// Start Tailwind in watch mode for faster incremental builds
|
|
63
|
-
export const startTailwindWatchMode = Effect.fn(function* (
|
|
64
|
-
projectRoot: string
|
|
65
|
-
) {
|
|
66
|
-
const glob = new Bun.Glob('**/*.css')
|
|
67
|
-
const files = Arr.fromIterable(
|
|
68
|
-
glob.scanSync({ absolute: true, cwd: projectRoot })
|
|
69
|
-
)
|
|
70
|
-
const entrypoint = Arr.head(files)
|
|
71
|
-
|
|
72
|
-
if (Option.isNone(entrypoint))
|
|
73
|
-
return yield* Effect.fail(new Error('No CSS files found for Tailwind CSS.'))
|
|
74
|
-
|
|
75
|
-
// Create a temp output file
|
|
76
|
-
const outputFile = `${projectRoot}/node_modules/.cache/render/tailwind.css`
|
|
77
|
-
|
|
78
|
-
// Ensure the cache directory exists
|
|
79
|
-
yield* Effect.tryPromise(async () => {
|
|
80
|
-
await Bun.write(outputFile, '')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
// Start Tailwind in watch mode - use 'inherit' for stderr to see errors
|
|
84
|
-
const proc = Bun.spawn(
|
|
85
|
-
[
|
|
86
|
-
'bunx',
|
|
87
|
-
'@tailwindcss/cli',
|
|
88
|
-
'-i',
|
|
89
|
-
entrypoint.value,
|
|
90
|
-
'-o',
|
|
91
|
-
outputFile,
|
|
92
|
-
'--minify',
|
|
93
|
-
'--watch'
|
|
94
|
-
],
|
|
95
|
-
{
|
|
96
|
-
cwd: projectRoot,
|
|
97
|
-
stdout: 'ignore',
|
|
98
|
-
stderr: 'inherit'
|
|
99
|
-
}
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
tailwindWatcher = { proc, outputFile }
|
|
103
|
-
|
|
104
|
-
// Wait for initial compilation and poll for file to have content
|
|
105
|
-
let attempts = 0
|
|
106
|
-
while (attempts < 20) {
|
|
107
|
-
yield* Effect.sleep('100 millis')
|
|
108
|
-
const file = Bun.file(outputFile)
|
|
109
|
-
const size = file.size
|
|
110
|
-
if (size > 0) {
|
|
111
|
-
const content = yield* Effect.tryPromise(() => file.text())
|
|
112
|
-
if (content.length > 0) {
|
|
113
|
-
cachedCssOutput = content
|
|
114
|
-
console.log(
|
|
115
|
-
` [Tailwind] Initial compilation: ${content.length} bytes`
|
|
116
|
-
)
|
|
117
|
-
break
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
attempts++
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { outputFile }
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
// Read the latest CSS from the watch mode output with retry
|
|
127
|
-
export const getWatchedCss = Effect.fn(function* () {
|
|
128
|
-
if (!tailwindWatcher) {
|
|
129
|
-
console.log(
|
|
130
|
-
' [CSS Debug] No tailwind watcher, using cached:',
|
|
131
|
-
(cachedCssOutput ?? '').length,
|
|
132
|
-
'bytes'
|
|
133
|
-
)
|
|
134
|
-
return cachedCssOutput ?? ''
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Try to read with retries (Tailwind might be mid-write)
|
|
138
|
-
let attempts = 0
|
|
139
|
-
while (attempts < 10) {
|
|
140
|
-
const css = yield* Effect.tryPromise(async () => {
|
|
141
|
-
if (!tailwindWatcher) return ''
|
|
142
|
-
const file = Bun.file(tailwindWatcher.outputFile)
|
|
143
|
-
return file.text()
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
if (css.length > 0) {
|
|
147
|
-
cachedCssOutput = css // Update cache
|
|
148
|
-
console.log(' [CSS Debug] Read from watcher:', css.length, 'bytes')
|
|
149
|
-
return css
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// File is empty, Tailwind might be recompiling - wait and retry
|
|
153
|
-
yield* Effect.sleep('50 millis')
|
|
154
|
-
attempts++
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Fall back to cached if file is still empty
|
|
158
|
-
console.log(
|
|
159
|
-
' [CSS Debug] File empty, using cached:',
|
|
160
|
-
(cachedCssOutput ?? '').length,
|
|
161
|
-
'bytes'
|
|
162
|
-
)
|
|
163
|
-
return cachedCssOutput ?? ''
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
// Stop the Tailwind watcher
|
|
167
|
-
export const stopTailwindWatcher = () => {
|
|
168
|
-
if (tailwindWatcher) {
|
|
169
|
-
tailwindWatcher.proc.kill()
|
|
170
|
-
tailwindWatcher = null
|
|
171
|
-
}
|
|
172
|
-
}
|
package/src/core/html.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Effect } from 'effect'
|
|
2
|
-
import type { CSS } from './css/css'
|
|
3
|
-
|
|
4
|
-
export const mountHTML = ({ element, css, bundledScript }: MountHTMLArgs) =>
|
|
5
|
-
Effect.sync(
|
|
6
|
-
() =>
|
|
7
|
-
`<!DOCTYPE html>
|
|
8
|
-
<html lang="en">
|
|
9
|
-
<head>
|
|
10
|
-
<meta charset="UTF-8">
|
|
11
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
12
|
-
<title>Component Preview</title>
|
|
13
|
-
${css.map((style) => `<style>${style.compiledOutput}</style>`).join('\n')}
|
|
14
|
-
<style>
|
|
15
|
-
body { margin: 0; padding: 1rem; }
|
|
16
|
-
</style>
|
|
17
|
-
</head>
|
|
18
|
-
<body>
|
|
19
|
-
<div id="root">${element}</div>
|
|
20
|
-
${bundledScript ? `<script type="module">${bundledScript.replace(/<\/script>/gi, '<\\/script>')}</script>` : ''}
|
|
21
|
-
<script>
|
|
22
|
-
// HMR client for iframe
|
|
23
|
-
(function() {
|
|
24
|
-
let ws;
|
|
25
|
-
let reconnectAttempts = 0;
|
|
26
|
-
|
|
27
|
-
function connect() {
|
|
28
|
-
ws = new WebSocket('ws://' + location.host + '/__hmr');
|
|
29
|
-
|
|
30
|
-
ws.onopen = function() {
|
|
31
|
-
reconnectAttempts = 0;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
ws.onmessage = function(event) {
|
|
35
|
-
const data = JSON.parse(event.data);
|
|
36
|
-
if (data.type === 'reload') {
|
|
37
|
-
location.reload();
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
ws.onclose = function() {
|
|
42
|
-
if (reconnectAttempts < 10) {
|
|
43
|
-
reconnectAttempts++;
|
|
44
|
-
setTimeout(connect, 1000 * Math.min(reconnectAttempts, 5));
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
ws.onerror = function() { ws.close(); };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
connect();
|
|
52
|
-
})();
|
|
53
|
-
</script>
|
|
54
|
-
</body>
|
|
55
|
-
</html>
|
|
56
|
-
`
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
type MountHTMLArgs = {
|
|
60
|
-
element: string
|
|
61
|
-
css: CSS[]
|
|
62
|
-
bundledScript?: string
|
|
63
|
-
}
|
package/src/core/pkg.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { FileSystem, Path } from '@effect/platform'
|
|
2
|
-
import { Effect, Option, Schema } from 'effect'
|
|
3
|
-
|
|
4
|
-
const packageJsonSchema = Schema.Struct({
|
|
5
|
-
dependencies: Schema.Record({
|
|
6
|
-
key: Schema.String,
|
|
7
|
-
value: Schema.String
|
|
8
|
-
}).pipe(Schema.optional),
|
|
9
|
-
devDependencies: Schema.Record({
|
|
10
|
-
key: Schema.String,
|
|
11
|
-
value: Schema.String
|
|
12
|
-
}).pipe(Schema.optional),
|
|
13
|
-
peerDependencies: Schema.Record({
|
|
14
|
-
key: Schema.String,
|
|
15
|
-
value: Schema.String
|
|
16
|
-
}).pipe(Schema.optional)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
export const findNearestPackageJson = Effect.fn(function* (startPath: string) {
|
|
20
|
-
let aux: string | null = null
|
|
21
|
-
const path = yield* Path.Path
|
|
22
|
-
const fs = yield* FileSystem.FileSystem
|
|
23
|
-
|
|
24
|
-
while (startPath) {
|
|
25
|
-
const candidatePath = path.join(startPath, 'package.json')
|
|
26
|
-
|
|
27
|
-
const exists = yield* fs.exists(candidatePath)
|
|
28
|
-
|
|
29
|
-
if (exists) {
|
|
30
|
-
const content = yield* fs.readFileString(candidatePath)
|
|
31
|
-
|
|
32
|
-
const contentAsJSON = yield* Effect.try(() => JSON.parse(content))
|
|
33
|
-
|
|
34
|
-
const parsed =
|
|
35
|
-
yield* Schema.decodeUnknown(packageJsonSchema)(contentAsJSON)
|
|
36
|
-
|
|
37
|
-
return Option.some(parsed)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const parentDir = path.dirname(startPath)
|
|
41
|
-
|
|
42
|
-
startPath = parentDir
|
|
43
|
-
|
|
44
|
-
if (parentDir === aux) break
|
|
45
|
-
|
|
46
|
-
aux = startPath
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return Option.none()
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
export const findNearestNodeModules = Effect.fn(function* (startPath: string) {
|
|
53
|
-
let aux: string | null = null
|
|
54
|
-
const path = yield* Path.Path
|
|
55
|
-
const fs = yield* FileSystem.FileSystem
|
|
56
|
-
|
|
57
|
-
while (startPath) {
|
|
58
|
-
const candidatePath = path.join(startPath, 'node_modules')
|
|
59
|
-
|
|
60
|
-
const exists = yield* fs.exists(candidatePath)
|
|
61
|
-
|
|
62
|
-
if (exists) {
|
|
63
|
-
return Option.some(candidatePath)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const parentDir = path.dirname(startPath)
|
|
67
|
-
|
|
68
|
-
startPath = parentDir
|
|
69
|
-
|
|
70
|
-
if (parentDir === aux) break
|
|
71
|
-
|
|
72
|
-
aux = startPath
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return Option.none()
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
export const getProjectDependencies = Effect.fn(function* (
|
|
79
|
-
projectRoot: string
|
|
80
|
-
) {
|
|
81
|
-
const pkg = yield* findNearestPackageJson(projectRoot)
|
|
82
|
-
|
|
83
|
-
if (Option.isNone(pkg)) return []
|
|
84
|
-
|
|
85
|
-
const deps = [
|
|
86
|
-
...Object.keys(pkg.value.dependencies || {}),
|
|
87
|
-
...Object.keys(pkg.value.devDependencies || {}),
|
|
88
|
-
...Object.keys(pkg.value.peerDependencies || {})
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
return deps
|
|
92
|
-
})
|
package/src/core/story.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { Array as Arr, Effect } from 'effect'
|
|
2
|
-
import type { ReactStory } from '#react/reactStory'
|
|
3
|
-
import { importStoryWithBundler } from './bundler'
|
|
4
|
-
|
|
5
|
-
export const loadStories = Effect.fn(function* (absolutePath: string) {
|
|
6
|
-
const glob = new Bun.Glob('**/*.stories.{ts,tsx,js,jsx,mts,cts}')
|
|
7
|
-
|
|
8
|
-
const files = Arr.fromIterable(
|
|
9
|
-
glob.scanSync({ absolute: true, cwd: absolutePath })
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
const tasks = Arr.map(files, (file) =>
|
|
13
|
-
importStoryWithBundler({
|
|
14
|
-
storyPath: file,
|
|
15
|
-
projectRoot: absolutePath
|
|
16
|
-
}).pipe(
|
|
17
|
-
Effect.scoped,
|
|
18
|
-
Effect.map((mod) => ({ mod, sourcePath: file }))
|
|
19
|
-
)
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
const results = yield* Effect.all(tasks, { concurrency: 'unbounded' })
|
|
23
|
-
|
|
24
|
-
const stories = Arr.flatMap(results, ({ mod, sourcePath }) =>
|
|
25
|
-
Object.values(mod).map((story) => {
|
|
26
|
-
if (
|
|
27
|
-
story !== null &&
|
|
28
|
-
typeof story === 'object' &&
|
|
29
|
-
'~type' in story &&
|
|
30
|
-
story['~type'] === 'ReactStory'
|
|
31
|
-
) {
|
|
32
|
-
// Attach the source path for browser bundling
|
|
33
|
-
;(story as ReactStory<React.ComponentType>).sourcePath = sourcePath
|
|
34
|
-
}
|
|
35
|
-
return story
|
|
36
|
-
})
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
return stories.filter(
|
|
40
|
-
// biome-ignore lint/suspicious/noExplicitAny: Required for story filtering
|
|
41
|
-
(story): story is ReactStory<any> =>
|
|
42
|
-
story !== null &&
|
|
43
|
-
typeof story === 'object' &&
|
|
44
|
-
'~type' in story &&
|
|
45
|
-
story['~type'] === 'ReactStory'
|
|
46
|
-
)
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
export interface Story {
|
|
50
|
-
// biome-ignore lint/suspicious/noExplicitAny: Required for generic story execution
|
|
51
|
-
render: (...args: any[]) => unknown
|
|
52
|
-
}
|
package/src/core/tsconfig.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { FileSystem, Path } from '@effect/platform'
|
|
2
|
-
import { Effect, Option, Schema } from 'effect'
|
|
3
|
-
|
|
4
|
-
const tsconfigSchema = Schema.Struct({
|
|
5
|
-
compilerOptions: Schema.Struct({
|
|
6
|
-
baseUrl: Schema.String.pipe(Schema.optional),
|
|
7
|
-
paths: Schema.Record({
|
|
8
|
-
key: Schema.String,
|
|
9
|
-
value: Schema.Array(Schema.String)
|
|
10
|
-
}).pipe(Schema.optional)
|
|
11
|
-
})
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
const candidates = ['tsconfig.app.json', 'tsconfig.base.json', 'tsconfig.json']
|
|
15
|
-
|
|
16
|
-
export const findNearestTsconfig = Effect.fn(function* (startPath: string) {
|
|
17
|
-
let aux: string | null = null
|
|
18
|
-
const path = yield* Path.Path
|
|
19
|
-
const fs = yield* FileSystem.FileSystem
|
|
20
|
-
|
|
21
|
-
while (startPath) {
|
|
22
|
-
for (const candidate of candidates) {
|
|
23
|
-
const tsconfigPath = path.join(startPath, candidate)
|
|
24
|
-
|
|
25
|
-
const exists = yield* fs.exists(tsconfigPath)
|
|
26
|
-
|
|
27
|
-
if (exists) {
|
|
28
|
-
const content = yield* Effect.tryPromise(() => import(tsconfigPath))
|
|
29
|
-
|
|
30
|
-
const parsed = yield* Schema.decodeUnknown(tsconfigSchema)(content)
|
|
31
|
-
|
|
32
|
-
if (parsed.compilerOptions?.paths) return Option.some(tsconfigPath)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const parentDir = path.dirname(startPath)
|
|
37
|
-
|
|
38
|
-
startPath = parentDir
|
|
39
|
-
|
|
40
|
-
if (parentDir === aux) break
|
|
41
|
-
|
|
42
|
-
aux = startPath
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return Option.none()
|
|
46
|
-
})
|
package/src/react/react.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import type { ComponentType } from 'react'
|
|
2
|
-
import { ArrayControl } from '#core/control/arrayControl'
|
|
3
|
-
import { BooleanControl } from '#core/control/booleanControl'
|
|
4
|
-
import type { Control } from '#core/control/control'
|
|
5
|
-
import type {
|
|
6
|
-
ArrayPropNames,
|
|
7
|
-
BooleanPropNames,
|
|
8
|
-
ControlBuilder,
|
|
9
|
-
ExtractStringLiteralUnion,
|
|
10
|
-
NumberPropNames,
|
|
11
|
-
StringPropNames,
|
|
12
|
-
VariantPropNames
|
|
13
|
-
} from '#core/control/controlBuilder'
|
|
14
|
-
import { NumberControl } from '#core/control/numberControl'
|
|
15
|
-
import { StringControl } from '#core/control/stringControl'
|
|
16
|
-
import { VariantControl } from '#core/control/variantControl'
|
|
17
|
-
import type { ReactStory } from './reactStory'
|
|
18
|
-
|
|
19
|
-
export function controls<
|
|
20
|
-
// biome-ignore lint/suspicious/noExplicitAny: Required for React component prop inference
|
|
21
|
-
Component extends React.ComponentType<any>
|
|
22
|
-
>(): ReactControlBuilder<React.ComponentProps<Component>, never> {
|
|
23
|
-
return new ReactControlBuilder<React.ComponentProps<Component>, never>()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const CONTROLS_SYMBOL = Symbol.for('@renderweaver/controls')
|
|
27
|
-
|
|
28
|
-
export class ReactControlBuilder<Props, Used extends keyof Props>
|
|
29
|
-
implements ControlBuilder<Props, Used>
|
|
30
|
-
{
|
|
31
|
-
private controls: Control[] = []
|
|
32
|
-
|
|
33
|
-
public [CONTROLS_SYMBOL](): Control[] {
|
|
34
|
-
return this.controls
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
public array<
|
|
38
|
-
Name extends Exclude<ArrayPropNames<Props>, Used> & string,
|
|
39
|
-
Type = Props[Name] extends Array<infer U> ? U : never
|
|
40
|
-
>(
|
|
41
|
-
name: Name,
|
|
42
|
-
options: {
|
|
43
|
-
defaultValue: Props[Name]
|
|
44
|
-
}
|
|
45
|
-
): ControlBuilder<Props, Used | Name> {
|
|
46
|
-
this.controls.push(
|
|
47
|
-
new ArrayControl<Type>({
|
|
48
|
-
name,
|
|
49
|
-
defaultValue: options.defaultValue as Type[]
|
|
50
|
-
})
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
return this as unknown as ControlBuilder<Props, Used | Name>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
public bool<Name extends Exclude<BooleanPropNames<Props>, Used> & string>(
|
|
57
|
-
name: Name,
|
|
58
|
-
options: {
|
|
59
|
-
defaultValue: boolean
|
|
60
|
-
}
|
|
61
|
-
): ControlBuilder<Props, Used | Name> {
|
|
62
|
-
this.controls.push(
|
|
63
|
-
new BooleanControl({
|
|
64
|
-
name,
|
|
65
|
-
defaultValue: options.defaultValue
|
|
66
|
-
})
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
return this as unknown as ControlBuilder<Props, Used | Name>
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
public number<Name extends Exclude<NumberPropNames<Props>, Used> & string>(
|
|
73
|
-
name: Name,
|
|
74
|
-
options: {
|
|
75
|
-
defaultValue: number
|
|
76
|
-
}
|
|
77
|
-
): ControlBuilder<Props, Used | Name> {
|
|
78
|
-
this.controls.push(
|
|
79
|
-
new NumberControl({
|
|
80
|
-
name,
|
|
81
|
-
defaultValue: options.defaultValue
|
|
82
|
-
})
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
return this as unknown as ControlBuilder<Props, Used | Name>
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
public string<Name extends Exclude<StringPropNames<Props>, Used> & string>(
|
|
89
|
-
name: Name,
|
|
90
|
-
options: { defaultValue: string }
|
|
91
|
-
): ControlBuilder<Props, Used | Name> {
|
|
92
|
-
this.controls.push(
|
|
93
|
-
new StringControl({
|
|
94
|
-
name,
|
|
95
|
-
defaultValue: options.defaultValue
|
|
96
|
-
})
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
return this as unknown as ControlBuilder<Props, Used | Name>
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
public variant<Name extends Exclude<VariantPropNames<Props>, Used> & string>(
|
|
103
|
-
name: Name,
|
|
104
|
-
options: {
|
|
105
|
-
defaultValue: ExtractStringLiteralUnion<Props[Name]>
|
|
106
|
-
options: ExtractStringLiteralUnion<Props[Name]>[]
|
|
107
|
-
}
|
|
108
|
-
): ControlBuilder<Props, Used | Name> {
|
|
109
|
-
this.controls.push(
|
|
110
|
-
new VariantControl({
|
|
111
|
-
name: name,
|
|
112
|
-
options: options.options as string[],
|
|
113
|
-
defaultValue: options.defaultValue as string
|
|
114
|
-
})
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
return this as unknown as ControlBuilder<Props, Used | Name>
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function getDefaultPropsForReactComponent<
|
|
122
|
-
// biome-ignore lint/suspicious/noExplicitAny: Required for React component prop inference
|
|
123
|
-
Component extends ComponentType<any>
|
|
124
|
-
>(story: ReactStory<Component>): React.ComponentProps<Component> {
|
|
125
|
-
if (!story.controls) {
|
|
126
|
-
return {} as React.ComponentProps<Component>
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {} as React.ComponentProps<Component>
|
|
130
|
-
}
|
package/src/react/reactStory.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// biome-ignore-all lint/suspicious/noExplicitAny: Required for React component prop inference
|
|
2
|
-
import type { UnfinishedControls } from '#core/control/controlBuilder'
|
|
3
|
-
import type { Story } from '#core/story'
|
|
4
|
-
|
|
5
|
-
export function story<Component extends React.ComponentType<any>>(
|
|
6
|
-
args: StoryArgs<Component>
|
|
7
|
-
) {
|
|
8
|
-
return new ReactStory(args)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class ReactStory<Component extends React.ComponentType<any>>
|
|
12
|
-
implements Story
|
|
13
|
-
{
|
|
14
|
-
public name: string
|
|
15
|
-
public id: string
|
|
16
|
-
public component: React.ComponentType<any> | undefined
|
|
17
|
-
public render: (props: React.ComponentProps<Component>) => React.ReactElement
|
|
18
|
-
public controls: UnfinishedControls | undefined
|
|
19
|
-
public '~type' = 'ReactStory'
|
|
20
|
-
public sourcePath?: string // Path to the story source file for browser bundling
|
|
21
|
-
|
|
22
|
-
public constructor(args: StoryArgs<Component>) {
|
|
23
|
-
this.name = args.name
|
|
24
|
-
this.component = args.component
|
|
25
|
-
this.render = args.render
|
|
26
|
-
this.controls = args.controls
|
|
27
|
-
this.id = this.name.replace(/\s+/g, '-').toLowerCase()
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type StoryArgs<Component extends React.ComponentType<any>> = {
|
|
32
|
-
name: string
|
|
33
|
-
render: (props: React.ComponentProps<Component>) => React.ReactElement
|
|
34
|
-
component?: Component
|
|
35
|
-
controls?: UnfinishedControls
|
|
36
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Effect } from 'effect'
|
|
2
|
-
import type { ReactStory } from '#react/reactStory'
|
|
3
|
-
|
|
4
|
-
export const getStories = ({ stories }: GetStoriesArgs) =>
|
|
5
|
-
Effect.sync(() => {
|
|
6
|
-
const categorizedStories: Record<string, SimpleStory[]> = {}
|
|
7
|
-
|
|
8
|
-
for (const story of stories) {
|
|
9
|
-
const [category, storyName] = story.name.split('/')
|
|
10
|
-
|
|
11
|
-
if (!category || !storyName) continue
|
|
12
|
-
|
|
13
|
-
if (!categorizedStories[category]) {
|
|
14
|
-
categorizedStories[category] = []
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
categorizedStories[category].push({
|
|
18
|
-
name: storyName,
|
|
19
|
-
path: story.id
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const sortedCategories = Object.keys(categorizedStories).sort()
|
|
24
|
-
|
|
25
|
-
for (const category of sortedCategories) {
|
|
26
|
-
categorizedStories[category]?.sort((a, b) => a.name.localeCompare(b.name))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return new Response(JSON.stringify(categorizedStories), {
|
|
30
|
-
status: 200,
|
|
31
|
-
headers: { 'Content-Type': 'application/json' }
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
type SimpleStory = {
|
|
36
|
-
name: string
|
|
37
|
-
path: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
type GetStoriesArgs = {
|
|
41
|
-
request: Request
|
|
42
|
-
// biome-ignore lint/suspicious/noExplicitAny: Required for ReactStory type
|
|
43
|
-
stories: ReactStory<any>[]
|
|
44
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { BunRequest } from 'bun'
|
|
2
|
-
import { Effect } from 'effect'
|
|
3
|
-
import { renderToReadableStream } from 'react-dom/server'
|
|
4
|
-
import { bundleStoryForBrowser } from '#core/bundler'
|
|
5
|
-
import type { CSS } from '#core/css/css'
|
|
6
|
-
import { mountHTML } from '#core/html'
|
|
7
|
-
import { getDefaultPropsForReactComponent } from '#react/reactControlBuilder'
|
|
8
|
-
import type { ReactStory } from '#react/reactStory'
|
|
9
|
-
import { streamToString } from '#server/streaming'
|
|
10
|
-
|
|
11
|
-
export const renderIframe = Effect.fn(function* ({
|
|
12
|
-
request,
|
|
13
|
-
css,
|
|
14
|
-
stories,
|
|
15
|
-
projectRoot
|
|
16
|
-
}: RenderIframeArgs) {
|
|
17
|
-
const storyId = request.url.split('/iframe/')[1]?.split('?')[0] // Strip query params
|
|
18
|
-
const story = stories.find((s) => s.id === storyId)
|
|
19
|
-
|
|
20
|
-
if (!story) return new Response('Story not found', { status: 404 })
|
|
21
|
-
|
|
22
|
-
const defaultProps = getDefaultPropsForReactComponent(story)
|
|
23
|
-
|
|
24
|
-
const element = story.render(defaultProps)
|
|
25
|
-
|
|
26
|
-
const stream = yield* Effect.tryPromise(() => renderToReadableStream(element))
|
|
27
|
-
|
|
28
|
-
const html = yield* streamToString(stream)
|
|
29
|
-
|
|
30
|
-
// Bundle the story for browser hydration
|
|
31
|
-
let bundledScript: string | undefined
|
|
32
|
-
if (story.sourcePath) {
|
|
33
|
-
const bundleResult = yield* bundleStoryForBrowser({
|
|
34
|
-
storyPath: story.sourcePath,
|
|
35
|
-
projectRoot,
|
|
36
|
-
storyId: story.id
|
|
37
|
-
}).pipe(Effect.option)
|
|
38
|
-
|
|
39
|
-
if (bundleResult._tag === 'Some') {
|
|
40
|
-
bundledScript = bundleResult.value
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const mountedHTML = yield* mountHTML({
|
|
45
|
-
css,
|
|
46
|
-
element: html,
|
|
47
|
-
bundledScript
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
return new Response(mountedHTML, {
|
|
51
|
-
headers: {
|
|
52
|
-
'Content-Type': 'text/html',
|
|
53
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
54
|
-
Pragma: 'no-cache',
|
|
55
|
-
Expires: '0'
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
type RenderIframeArgs = {
|
|
61
|
-
request: BunRequest<'iframe/*'>
|
|
62
|
-
// biome-ignore lint/suspicious/noExplicitAny: Required for ReactStory type
|
|
63
|
-
stories: ReactStory<any>[]
|
|
64
|
-
css: CSS[]
|
|
65
|
-
projectRoot: string
|
|
66
|
-
}
|