@alstar/studio 0.0.0-beta.14 → 0.0.0-beta.16
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/bin/build-ssg.ts +42 -0
- package/components/BlockFieldRenderer.ts +1 -1
- package/components/FieldRenderer.ts +7 -2
- package/components/Render.ts +6 -1
- package/components/fields/Reference.ts +67 -0
- package/components/fields/index.ts +2 -1
- package/index.ts +9 -3
- package/package.json +6 -3
- package/public/studio/main.css +1 -0
- package/queries/block-2.ts +15 -15
- package/schemas.ts +1 -1
- package/types.ts +17 -10
- package/utils/get-or-create-row.ts +2 -1
package/bin/build-ssg.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { type SSGPlugin, toSSG } from 'hono/ssg'
|
|
4
|
+
import fs from 'fs/promises'
|
|
5
|
+
|
|
6
|
+
const root = path.resolve('.')
|
|
7
|
+
|
|
8
|
+
let app
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
app = (await import(path.join(root, 'index.ts'))).default
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.log(error)
|
|
14
|
+
console.log('Found no app')
|
|
15
|
+
console.log('Main app needs to be the default export in ./index.ts')
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await existingStaticFolder()
|
|
20
|
+
|
|
21
|
+
const excludeStudioPlugin: SSGPlugin = {
|
|
22
|
+
beforeRequestHook: (req) => {
|
|
23
|
+
if (!req.url.includes('/studio/')) {
|
|
24
|
+
return req
|
|
25
|
+
}
|
|
26
|
+
return false
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await toSSG(app, fs, { plugins: [excludeStudioPlugin] })
|
|
31
|
+
|
|
32
|
+
await fs.cp(path.join(root, 'public'), path.join(root, 'static'), {
|
|
33
|
+
recursive: true,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
console.log('🚀 Done generating static build')
|
|
37
|
+
|
|
38
|
+
process.exit(0)
|
|
39
|
+
|
|
40
|
+
async function existingStaticFolder() {
|
|
41
|
+
return fs.rm(path.join(root, 'static'), { recursive: true }).catch(() => {})
|
|
42
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Field } from './fields/index.ts'
|
|
2
2
|
import type { BlocksFieldDefStructure, FieldDefStructure } from '../types.ts'
|
|
3
3
|
import BlockFieldRenderer from './BlockFieldRenderer.ts'
|
|
4
|
+
import { BlockFieldInstance } from '../utils/define.ts'
|
|
4
5
|
|
|
5
6
|
export default (props: {
|
|
6
7
|
entryId: number
|
|
@@ -11,6 +12,10 @@ export default (props: {
|
|
|
11
12
|
}) => {
|
|
12
13
|
const { entryId, parentId, structure, name, id } = props
|
|
13
14
|
|
|
15
|
+
if (structure.instanceOf === BlockFieldInstance) {
|
|
16
|
+
return BlockFieldRenderer({ entryId, parentId, name, structure, id })
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
switch (structure.type) {
|
|
15
20
|
case 'text': {
|
|
16
21
|
return Field.Text({ entryId, parentId, name, id, structure })
|
|
@@ -28,8 +33,8 @@ export default (props: {
|
|
|
28
33
|
return Field.Text({ entryId, parentId, name, structure, id })
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
case '
|
|
32
|
-
return
|
|
36
|
+
case 'reference': {
|
|
37
|
+
return Field.Reference({ entryId, parentId, name, structure, id })
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
}
|
package/components/Render.ts
CHANGED
|
@@ -41,6 +41,11 @@ export default (props: {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
} catch (error) {
|
|
44
|
-
|
|
44
|
+
console.log(error)
|
|
45
|
+
|
|
46
|
+
return html`
|
|
47
|
+
<p>Error rendering "${name}"</p>
|
|
48
|
+
<pre><code>${JSON.stringify(error, null, 2)}</code></pre>
|
|
49
|
+
`
|
|
45
50
|
}
|
|
46
51
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getOrCreateRow } from '../../utils/get-or-create-row.ts'
|
|
2
|
+
import { html } from '../../utils/html.ts'
|
|
3
|
+
import type { FieldDefStructure, ReferenceFieldStructure } from '../../types.ts'
|
|
4
|
+
import { query } from '../../queries/index.ts'
|
|
5
|
+
|
|
6
|
+
export default (props: {
|
|
7
|
+
entryId: number
|
|
8
|
+
parentId: number
|
|
9
|
+
name: string
|
|
10
|
+
id?: number
|
|
11
|
+
structure: ReferenceFieldStructure
|
|
12
|
+
sortOrder?: number
|
|
13
|
+
}) => {
|
|
14
|
+
const { entryId, parentId, name, structure, sortOrder = 0, id } = props
|
|
15
|
+
|
|
16
|
+
const data = getOrCreateRow({
|
|
17
|
+
parentId,
|
|
18
|
+
name,
|
|
19
|
+
field: structure,
|
|
20
|
+
sortOrder,
|
|
21
|
+
id,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (!data) return html`<p>No block</p>`
|
|
25
|
+
|
|
26
|
+
const entries = query.roots({ type: structure.to }, { depth: 1 })
|
|
27
|
+
|
|
28
|
+
return html`
|
|
29
|
+
<form
|
|
30
|
+
data-on-input="@patch('/studio/api/block', {
|
|
31
|
+
contentType: 'form',
|
|
32
|
+
headers: {
|
|
33
|
+
render: ''
|
|
34
|
+
}
|
|
35
|
+
})"
|
|
36
|
+
>
|
|
37
|
+
<hgroup>
|
|
38
|
+
<label for="block-${data.id}">${structure.label}</label>
|
|
39
|
+
${structure.description &&
|
|
40
|
+
html`
|
|
41
|
+
<p><small>${structure.description}</small></p>
|
|
42
|
+
`}
|
|
43
|
+
</hgroup>
|
|
44
|
+
|
|
45
|
+
<input
|
|
46
|
+
list="entries-${data.id}"
|
|
47
|
+
id="block-${data.id}"
|
|
48
|
+
name="value"
|
|
49
|
+
value="${data.value}"
|
|
50
|
+
/>
|
|
51
|
+
<datalist id="entries-${data.id}">
|
|
52
|
+
${entries.map((entry) => {
|
|
53
|
+
return html`<option value="${entry.fields.slug.value}">
|
|
54
|
+
${entry.fields.title.value}
|
|
55
|
+
</option>`
|
|
56
|
+
})}
|
|
57
|
+
</datalist>
|
|
58
|
+
|
|
59
|
+
<input type="hidden" name="id" value="${data.id}" />
|
|
60
|
+
|
|
61
|
+
<!-- <input type="hidden" name="type" value="{structure.type}" />
|
|
62
|
+
<input type="hidden" name="entryId" value="{entryId}" />
|
|
63
|
+
<input type="hidden" name="parentId" value="{parentId}" />
|
|
64
|
+
<input type="hidden" name="name" value="{name}" /> -->
|
|
65
|
+
</form>
|
|
66
|
+
`
|
|
67
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Text from './Text.ts'
|
|
2
2
|
import Markdown from './Markdown.ts'
|
|
3
3
|
import Slug, { routes as slugRoutes } from './Slug.ts'
|
|
4
|
+
import Reference from './Reference.ts'
|
|
4
5
|
|
|
5
|
-
export const Field = { Text, Markdown, Slug }
|
|
6
|
+
export const Field = { Text, Markdown, Slug, Reference }
|
|
6
7
|
|
|
7
8
|
export const fieldRoutes = [slugRoutes]
|
package/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { getConfig } from './utils/get-config.ts'
|
|
|
14
14
|
import startupLog from './utils/startup-log.ts'
|
|
15
15
|
import { apiRoutes } from './api/index.ts'
|
|
16
16
|
import { mcpRoutes } from './api/mcp.ts'
|
|
17
|
+
import packageJSON from './package.json' with { type: 'json' }
|
|
17
18
|
|
|
18
19
|
import auth from './utils/auth.ts'
|
|
19
20
|
import ErrorPage from './pages/error.ts'
|
|
@@ -25,6 +26,7 @@ export let rootdir = './node_modules/@alstar/studio'
|
|
|
25
26
|
export let studioStructure: types.Structure = {}
|
|
26
27
|
export let studioConfig: types.StudioConfig = {
|
|
27
28
|
siteName: '',
|
|
29
|
+
fileBasedRouter: true,
|
|
28
30
|
port: 3000,
|
|
29
31
|
structure: {},
|
|
30
32
|
}
|
|
@@ -72,8 +74,10 @@ const createStudio = async (config: types.StudioConfig) => {
|
|
|
72
74
|
/**
|
|
73
75
|
* User pages
|
|
74
76
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
if (studioConfig.fileBasedRouter) {
|
|
78
|
+
const pages = await fileBasedRouter('./pages')
|
|
79
|
+
if (pages) app.route('/', pages)
|
|
80
|
+
}
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
83
|
* Error pages
|
|
@@ -95,7 +99,8 @@ const createStudio = async (config: types.StudioConfig) => {
|
|
|
95
99
|
'/studio/backups/*',
|
|
96
100
|
serveStatic({
|
|
97
101
|
root: './',
|
|
98
|
-
rewriteRequestPath: (path) =>
|
|
102
|
+
rewriteRequestPath: (path) =>
|
|
103
|
+
path.replace(/^\/studio\/backups/, '/backups'),
|
|
99
104
|
}),
|
|
100
105
|
)
|
|
101
106
|
|
|
@@ -141,3 +146,4 @@ export { type RequestContext } from './types.ts'
|
|
|
141
146
|
export { createStudio }
|
|
142
147
|
export { html, type HtmlEscapedString } from './utils/html.ts'
|
|
143
148
|
export { query } from './queries/index.ts'
|
|
149
|
+
export const version = packageJSON.version
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alstar/studio",
|
|
3
|
-
"version": "0.0.0-beta.
|
|
3
|
+
"version": "0.0.0-beta.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"alstar": "./bin/build-ssg.ts"
|
|
8
|
+
},
|
|
6
9
|
"engines": {
|
|
7
10
|
"node": ">=23.8"
|
|
8
11
|
},
|
|
@@ -10,9 +13,9 @@
|
|
|
10
13
|
"@hono/node-server": "^1.18.1",
|
|
11
14
|
"@starfederation/datastar-sdk": "1.0.0-RC.1",
|
|
12
15
|
"hono": "^4.8.12",
|
|
13
|
-
"@alstar/refresher": "0.0.0-beta.3",
|
|
14
16
|
"@alstar/db": "0.0.0-beta.1",
|
|
15
|
-
"@alstar/
|
|
17
|
+
"@alstar/refresher": "0.0.0-beta.3",
|
|
18
|
+
"@alstar/ui": "0.0.0-beta.3"
|
|
16
19
|
},
|
|
17
20
|
"devDependencies": {
|
|
18
21
|
"@types/node": "^24.1.0",
|
package/public/studio/main.css
CHANGED
package/queries/block-2.ts
CHANGED
|
@@ -7,7 +7,7 @@ function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
|
|
|
7
7
|
const roots: DBBlockResult[] = []
|
|
8
8
|
|
|
9
9
|
for (const block of blocks) {
|
|
10
|
-
block.
|
|
10
|
+
block.blocks = []
|
|
11
11
|
map.set(block.id, block)
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -16,14 +16,14 @@ function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
|
|
|
16
16
|
roots.push(block)
|
|
17
17
|
} else {
|
|
18
18
|
const parent = map.get(block.parent_id)
|
|
19
|
-
if (parent) parent.
|
|
19
|
+
if (parent) parent.blocks!.push(block)
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// Sort
|
|
23
|
+
// Sort blocks by sort_order recursively
|
|
24
24
|
const sortChildren = (node: DBBlockResult) => {
|
|
25
|
-
node.
|
|
26
|
-
node.
|
|
25
|
+
node.blocks!.sort((a, b) => a.sort_order - b.sort_order)
|
|
26
|
+
node.blocks!.forEach(sortChildren)
|
|
27
27
|
}
|
|
28
28
|
roots.forEach(sortChildren)
|
|
29
29
|
|
|
@@ -35,7 +35,7 @@ function buildTree(blocks: DBBlockResult[]): DBBlockResult {
|
|
|
35
35
|
const roots: DBBlockResult[] = []
|
|
36
36
|
|
|
37
37
|
for (const block of blocks) {
|
|
38
|
-
block.
|
|
38
|
+
block.blocks = []
|
|
39
39
|
map.set(block.id, block)
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -44,14 +44,14 @@ function buildTree(blocks: DBBlockResult[]): DBBlockResult {
|
|
|
44
44
|
roots.push(block)
|
|
45
45
|
} else {
|
|
46
46
|
const parent = map.get(block.parent_id)
|
|
47
|
-
if (parent) parent.
|
|
47
|
+
if (parent) parent.blocks!.push(block)
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// Sort
|
|
51
|
+
// Sort blocks by sort_order recursively
|
|
52
52
|
const sortChildren = (node: DBBlockResult) => {
|
|
53
|
-
node.
|
|
54
|
-
node.
|
|
53
|
+
node.blocks!.sort((a, b) => a.sort_order - b.sort_order)
|
|
54
|
+
node.blocks!.forEach(sortChildren)
|
|
55
55
|
}
|
|
56
56
|
roots.forEach(sortChildren)
|
|
57
57
|
|
|
@@ -65,7 +65,7 @@ function transformBlocksTree(
|
|
|
65
65
|
const fields: Record<string, DBBlockResult> = {}
|
|
66
66
|
let hasFields = false
|
|
67
67
|
|
|
68
|
-
for (const child of block.
|
|
68
|
+
for (const child of block.blocks ?? []) {
|
|
69
69
|
const transformedChild = transformBlocksTree(
|
|
70
70
|
child,
|
|
71
71
|
child.type === 'blocks',
|
|
@@ -82,7 +82,7 @@ function transformBlocksTree(
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
if (!isBlocksChild) {
|
|
85
|
-
delete block.
|
|
85
|
+
delete block.blocks
|
|
86
86
|
} else {
|
|
87
87
|
delete block.fields
|
|
88
88
|
}
|
|
@@ -265,7 +265,7 @@ function buildTree2(items: DBRow[]): TODO | null {
|
|
|
265
265
|
|
|
266
266
|
let root: TODO | null = null;
|
|
267
267
|
|
|
268
|
-
// Second pass: assign
|
|
268
|
+
// Second pass: assign blocks to parents
|
|
269
269
|
for (const item of map.values()) {
|
|
270
270
|
if (item.parent_id === null) {
|
|
271
271
|
root = item; // Root node
|
|
@@ -273,8 +273,8 @@ function buildTree2(items: DBRow[]): TODO | null {
|
|
|
273
273
|
const parent = map.get(item.parent_id);
|
|
274
274
|
if (parent) {
|
|
275
275
|
if(parent.type === 'blocks') {
|
|
276
|
-
if (!parent.
|
|
277
|
-
parent.
|
|
276
|
+
if (!parent.blocks) parent.blocks = [];
|
|
277
|
+
parent.blocks.push(item);
|
|
278
278
|
} else {
|
|
279
279
|
if (!parent.fields) parent.fields = {};
|
|
280
280
|
parent.fields[item.name] = item;
|
package/schemas.ts
CHANGED
package/types.ts
CHANGED
|
@@ -21,14 +21,13 @@ export type DeepReadonly<T> =
|
|
|
21
21
|
export type PrimitiveField = {
|
|
22
22
|
name: string
|
|
23
23
|
label: string
|
|
24
|
-
type: 'text' | 'slug' | 'markdown' | 'image'
|
|
24
|
+
type: 'text' | 'slug' | 'markdown' | 'image' | 'reference'
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export type BlockField = {
|
|
28
28
|
name: string
|
|
29
29
|
label: string
|
|
30
|
-
|
|
31
|
-
children: Record<string, Field | Block>
|
|
30
|
+
blocks: Record<string, Field | Block>
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export type Field = PrimitiveField | BlockField
|
|
@@ -44,7 +43,7 @@ export type Structure = Record<string, BlockDefStructure>
|
|
|
44
43
|
// export type Structure = Record<string, BlockDefStructure>
|
|
45
44
|
|
|
46
45
|
// --- Field & block definitions ---
|
|
47
|
-
type FieldType = 'text' | 'slug' | 'markdown' | 'image'
|
|
46
|
+
type FieldType = 'text' | 'slug' | 'markdown' | 'image' | 'reference'
|
|
48
47
|
|
|
49
48
|
interface BaseField {
|
|
50
49
|
label: string
|
|
@@ -68,19 +67,27 @@ interface ImageFieldStructure extends ImageField {
|
|
|
68
67
|
instanceOf: typeof FieldInstance
|
|
69
68
|
}
|
|
70
69
|
|
|
70
|
+
interface ReferenceField extends BaseField {
|
|
71
|
+
type: 'reference'
|
|
72
|
+
to: string | string[]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ReferenceFieldStructure extends ReferenceField {
|
|
76
|
+
instanceOf: typeof FieldInstance
|
|
77
|
+
}
|
|
78
|
+
|
|
71
79
|
export interface BlocksFieldDef {
|
|
72
80
|
label: string
|
|
73
|
-
type: 'blocks'
|
|
74
81
|
description?: string
|
|
75
|
-
|
|
82
|
+
blocks: Record<string, BlockDefStructure | FieldDefStructure>
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
export interface BlocksFieldDefStructure extends BlocksFieldDef {
|
|
79
86
|
instanceOf: typeof BlockFieldInstance
|
|
80
87
|
}
|
|
81
88
|
|
|
82
|
-
export type FieldDef = TextField | ImageField
|
|
83
|
-
export type FieldDefStructure = TextFieldStructure | ImageFieldStructure
|
|
89
|
+
export type FieldDef = TextField | ImageField | ReferenceField
|
|
90
|
+
export type FieldDefStructure = TextFieldStructure | ImageFieldStructure | ReferenceFieldStructure
|
|
84
91
|
|
|
85
92
|
export interface BlockDef {
|
|
86
93
|
label: string
|
|
@@ -127,8 +134,7 @@ export type DBPrimitiveFieldResult = BaseDBResult & {
|
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
export type DBBlockFieldResult = BaseDBResult & {
|
|
130
|
-
|
|
131
|
-
children: DBBlockResult[]
|
|
137
|
+
blocks: DBBlockResult[]
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
export type DBBlockResult = BaseDBResult & {
|
|
@@ -153,6 +159,7 @@ export type BlockStatus = 'enabled' | 'disabled'
|
|
|
153
159
|
export type StudioConfig = {
|
|
154
160
|
siteName?: string
|
|
155
161
|
honoConfig?: HonoOptions<BlankEnv>
|
|
162
|
+
fileBasedRouter?: boolean,
|
|
156
163
|
port?: number
|
|
157
164
|
structure: Structure
|
|
158
165
|
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
BlocksFieldDefStructure,
|
|
6
6
|
FieldDefStructure,
|
|
7
7
|
} from '../types.ts'
|
|
8
|
+
import { BlockFieldInstance } from './define.ts'
|
|
8
9
|
|
|
9
10
|
export function getOrCreateRow(props: {
|
|
10
11
|
parentId: string | number | null
|
|
@@ -32,7 +33,7 @@ export function getOrCreateRow(props: {
|
|
|
32
33
|
const change = db.insertInto('blocks', {
|
|
33
34
|
name: name?.toString(),
|
|
34
35
|
label: field.label?.toString(),
|
|
35
|
-
type: field.type?.toString(),
|
|
36
|
+
type: field.instanceOf === BlockFieldInstance ? 'blocks' : field.type?.toString(),
|
|
36
37
|
sort_order: sortOrder,
|
|
37
38
|
parent_id: parentId,
|
|
38
39
|
})
|