@alstar/studio 0.0.0-beta.15 → 0.0.0-beta.18

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 (52) hide show
  1. package/api/block.ts +0 -14
  2. package/components/AdminPanel.ts +11 -5
  3. package/components/BlockFieldRenderer.ts +26 -20
  4. package/components/BlockRenderer.ts +4 -4
  5. package/components/Entries.ts +1 -1
  6. package/components/Entry.ts +13 -7
  7. package/components/FieldRenderer.ts +14 -8
  8. package/components/LivePreview.ts +37 -0
  9. package/components/Render.ts +8 -3
  10. package/components/SiteLayout.ts +1 -4
  11. package/components/fields/Markdown.ts +10 -3
  12. package/components/fields/Reference.ts +71 -0
  13. package/components/fields/Slug.ts +6 -6
  14. package/components/fields/Text.ts +13 -8
  15. package/components/fields/index.ts +2 -1
  16. package/components/icons.ts +3 -0
  17. package/components/settings/ApiKeys.ts +4 -4
  18. package/components/settings/Backup.ts +3 -3
  19. package/components/settings/Users.ts +1 -1
  20. package/index.ts +11 -10
  21. package/package.json +5 -6
  22. package/pages/entry/[id].ts +7 -1
  23. package/pages/error.ts +7 -6
  24. package/pages/login.ts +1 -1
  25. package/pages/register.ts +2 -2
  26. package/public/studio/css/admin-panel.css +27 -9
  27. package/public/studio/css/blocks-field.css +25 -0
  28. package/public/studio/css/entry-page.css +4 -0
  29. package/public/studio/css/entry.css +35 -0
  30. package/public/studio/css/field.css +14 -0
  31. package/public/studio/css/live-preview.css +25 -0
  32. package/public/studio/css/settings.css +4 -0
  33. package/public/studio/js/live-preview.js +26 -0
  34. package/public/studio/js/markdown-editor.js +6 -0
  35. package/public/studio/js/sortable-list.js +6 -4
  36. package/public/studio/main.css +11 -12
  37. package/public/studio/main.js +1 -0
  38. package/queries/block.ts +127 -105
  39. package/queries/index.ts +3 -2
  40. package/schemas.ts +1 -1
  41. package/types.ts +51 -75
  42. package/utils/define.ts +3 -1
  43. package/utils/get-or-create-row.ts +2 -1
  44. package/utils/refresher.ts +56 -0
  45. package/utils/renderSSE.ts +8 -3
  46. package/utils/startup-log.ts +4 -4
  47. package/queries/block-2.ts +0 -339
  48. package/queries/db-types.ts +0 -15
  49. package/queries/getBlockTrees-2.ts +0 -71
  50. package/queries/getBlocks.ts +0 -214
  51. package/queries/structure-types.ts +0 -97
  52. package/utils/buildBlocksTree.ts +0 -44
package/queries/block.ts CHANGED
@@ -2,97 +2,69 @@ import { db } from '@alstar/db'
2
2
  import { sql } from '../utils/sql.ts'
3
3
  import { type DBBlockResult } from '../types.ts'
4
4
 
5
- function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
6
- const map = new Map<number, DBBlockResult>()
7
- const roots: DBBlockResult[] = []
8
-
9
- for (const block of blocks) {
10
- block.children = []
11
- map.set(block.id, block)
12
- }
13
-
14
- for (const block of blocks) {
15
- if (block.parent_id === null) {
16
- roots.push(block)
17
- } else {
18
- const parent = map.get(block.parent_id)
19
- if (parent) parent.children!.push(block)
20
- }
21
- }
22
-
23
- // Sort children by sort_order recursively
24
- const sortChildren = (node: DBBlockResult) => {
25
- node.children!.sort((a, b) => a.sort_order - b.sort_order)
26
- node.children!.forEach(sortChildren)
27
- }
28
- roots.forEach(sortChildren)
29
-
30
- return roots
31
- }
32
-
33
- function buildTree(blocks: DBBlockResult[]): DBBlockResult {
34
- const map = new Map<number, DBBlockResult>()
35
- const roots: DBBlockResult[] = []
36
-
37
- for (const block of blocks) {
38
- block.children = []
39
- map.set(block.id, block)
40
- }
41
-
42
- for (const block of blocks) {
43
- if (block.parent_id === null) {
44
- roots.push(block)
45
- } else {
46
- const parent = map.get(block.parent_id)
47
- if (parent) parent.children!.push(block)
48
- }
49
- }
50
-
51
- // Sort children by sort_order recursively
52
- const sortChildren = (node: DBBlockResult) => {
53
- node.children!.sort((a, b) => a.sort_order - b.sort_order)
54
- node.children!.forEach(sortChildren)
55
- }
56
- roots.forEach(sortChildren)
57
-
58
- return roots[0]
59
- }
60
-
61
- function transformBlocksTree(
62
- block: DBBlockResult,
63
- isBlocksChild?: boolean,
64
- ): DBBlockResult {
65
- const fields: Record<string, DBBlockResult> = {}
66
- let hasFields = false
67
-
68
- for (const child of block.children ?? []) {
69
- const transformedChild = transformBlocksTree(
70
- child,
71
- child.type === 'blocks',
72
- )
73
-
74
- if (!isBlocksChild) {
75
- hasFields = true
76
- fields[transformedChild.name] = transformedChild
77
- }
78
- }
79
-
80
- if(hasFields) {
81
- block.fields = fields
82
- }
5
+ // function buildForest(blocks: DBBlockResult[]): DBBlockResult[] {
6
+ // const map = new Map<number, DBBlockResult>()
7
+ // const roots: DBBlockResult[] = []
8
+
9
+ // for (const block of blocks) {
10
+ // block.blocks = []
11
+ // map.set(block.id, block)
12
+ // }
13
+
14
+ // for (const block of blocks) {
15
+ // if (block.parent_id === null) {
16
+ // roots.push(block)
17
+ // } else {
18
+ // const parent = map.get(block.parent_id)
19
+ // if (parent) parent.blocks!.push(block)
20
+ // }
21
+ // }
22
+
23
+ // // Sort blocks by sort_order recursively
24
+ // const sortChildren = (node: DBBlockResult) => {
25
+ // node.blocks!.sort((a, b) => a.sort_order - b.sort_order)
26
+ // node.blocks!.forEach(sortChildren)
27
+ // }
28
+ // roots.forEach(sortChildren)
29
+
30
+ // return roots
31
+ // }
32
+
33
+ // function transformBlocksTree(
34
+ // block: DBBlockResult,
35
+ // isBlocksChild?: boolean,
36
+ // ): DBBlockResult {
37
+ // const fields: Record<string, DBBlockResult> = {}
38
+ // let hasFields = false
39
+
40
+ // for (const child of block.blocks ?? []) {
41
+ // const transformedChild = transformBlocksTree(
42
+ // child,
43
+ // child.type === 'blocks',
44
+ // )
45
+
46
+ // if (!isBlocksChild) {
47
+ // hasFields = true
48
+ // fields[transformedChild.name] = transformedChild
49
+ // }
50
+ // }
51
+
52
+ // if(hasFields) {
53
+ // block.fields = fields
54
+ // }
83
55
 
84
- if (!isBlocksChild) {
85
- delete block.children
86
- } else {
87
- delete block.fields
88
- }
56
+ // if (!isBlocksChild) {
57
+ // delete block.blocks
58
+ // } else {
59
+ // delete block.fields
60
+ // }
89
61
 
90
- return block
91
- }
62
+ // return block
63
+ // }
92
64
 
93
- function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
94
- return blocks.map((block) => transformBlocksTree(block))
95
- }
65
+ // function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
66
+ // return blocks.map((block) => transformBlocksTree(block))
67
+ // }
96
68
 
97
69
  function rootQuery(filterSql: string, depthLimit?: number) {
98
70
  const depthLimitClause =
@@ -218,24 +190,72 @@ function buildFilterSql(params: Record<string, any>) {
218
190
  return { filterSql, sqlParams }
219
191
  }
220
192
 
221
- export function roots(
222
- params: Record<string, any>,
223
- options?: {
224
- depth?: number
225
- },
226
- ): DBBlockResult[] | [] {
227
- const { filterSql, sqlParams } = buildFilterSql(params)
193
+ // export function roots(
194
+ // params: Record<string, any>,
195
+ // options?: {
196
+ // depth?: number
197
+ // },
198
+ // ): DBBlockResult[] | [] {
199
+ // const { filterSql, sqlParams } = buildFilterSql(params)
200
+
201
+ // const query = rootQuery(filterSql, options?.depth)
202
+ // const rows = db.database
203
+ // .prepare(query)
204
+ // .all(sqlParams) as unknown as DBBlockResult[]
205
+
206
+ // if (!rows.length) return []
207
+
208
+ // const forest = buildForest(rows)
209
+
210
+ // return transformForest(forest)
211
+ // }
212
+
213
+ type DBRow = {
214
+ id: number
215
+ created_at: string
216
+ updated_at: string
217
+ name: string
218
+ label: string
219
+ type: string
220
+ sort_order: number
221
+ value: string | null
222
+ options: string | null
223
+ status: 'enabled' | 'disabled'
224
+ parent_id: number | null
225
+ depth: number
226
+ }
228
227
 
229
- const query = rootQuery(filterSql, options?.depth)
230
- const rows = db.database
231
- .prepare(query)
232
- .all(sqlParams) as unknown as DBBlockResult[]
228
+ type TODO = any
229
+
230
+ function buildTree2(items: DBRow[]): TODO | null {
231
+ const map = new Map<number, TODO>();
233
232
 
234
- if (!rows.length) return []
233
+ // First pass: clone items into map
234
+ for (const item of items) {
235
+ map.set(item.id, { ...item });
236
+ }
235
237
 
236
- const forest = buildForest(rows)
238
+ let root: TODO | null = null;
237
239
 
238
- return transformForest(forest)
240
+ // Second pass: assign blocks to parents
241
+ for (const item of map.values()) {
242
+ if (item.parent_id === null) {
243
+ root = item; // Root node
244
+ } else {
245
+ const parent = map.get(item.parent_id);
246
+ if (parent) {
247
+ if(parent.type === 'blocks') {
248
+ if (!parent.blocks) parent.blocks = [];
249
+ parent.blocks.push(item);
250
+ } else {
251
+ if (!parent.fields) parent.fields = {};
252
+ parent.fields[item.name] = item;
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ return root;
239
259
  }
240
260
 
241
261
  export function root(
@@ -247,13 +267,15 @@ export function root(
247
267
  const query = rootQuery(filterSql, options?.depth)
248
268
  const rows = db.database
249
269
  .prepare(query)
250
- .all(sqlParams) as unknown as DBBlockResult[]
270
+ .all(sqlParams) as unknown as DBRow[]
251
271
 
252
272
  if (!rows.length) return null
253
273
 
254
- const tree = buildTree(rows)
274
+ // const tree = buildTree(rows)
275
+
276
+ const tree = buildTree2(rows)
255
277
 
256
- return transformBlocksTree(tree)
278
+ return tree // transformBlocksTree(tree)
257
279
  }
258
280
 
259
281
  export function block(params: Record<string, any>) {
package/queries/index.ts CHANGED
@@ -1,2 +1,3 @@
1
- export * as query from './block-2.ts'
2
- export { getBlockTrees } from './getBlockTrees.ts'
1
+ export * as query from './block.ts'
2
+ export { getBlockTrees } from './getBlockTrees.ts'
3
+ export { blockWithChildren } from './block-with-children.ts'
package/schemas.ts CHANGED
@@ -16,7 +16,7 @@ export const blocksTable = {
16
16
  columns: sql`
17
17
  name TEXT not null,
18
18
  label TEXT not null,
19
- type TEXT not null,
19
+ type TEXT,
20
20
  value TEXT,
21
21
  options JSON,
22
22
  status TEXT default 'enabled',
package/types.ts CHANGED
@@ -7,51 +7,20 @@ import {
7
7
  BlockInstance,
8
8
  FieldInstance,
9
9
  } from './utils/define.ts'
10
+ import { type HtmlEscapedString } from 'hono/utils/html'
10
11
 
11
- // DeepReadonly utility type
12
- export type DeepReadonly<T> =
13
- T extends (...args: any[]) => any // functions stay as-is
14
- ? T
15
- : T extends any[] // arrays/tuples
16
- ? { [K in keyof T]: DeepReadonly<T[K]> }
17
- : T extends object // objects
18
- ? { [K in keyof T]: DeepReadonly<T[K]> }
19
- : T; // primitives
20
-
21
- export type PrimitiveField = {
22
- name: string
23
- label: string
24
- type: 'text' | 'slug' | 'markdown' | 'image'
25
- }
26
-
27
- export type BlockField = {
28
- name: string
29
- label: string
30
- type: 'blocks'
31
- children: Record<string, Field | Block>
32
- }
33
-
34
- export type Field = PrimitiveField | BlockField
35
-
36
- export type Block = {
37
- name: string
38
- label: string
39
- type: string
40
- fields: Record<string, Field | Block>
41
- }
42
-
43
- export type Structure = Record<string, BlockDefStructure>
44
- // export type Structure = Record<string, BlockDefStructure>
12
+ export type BlockStatus = 'enabled' | 'disabled'
45
13
 
46
- // --- Field & block definitions ---
47
- type FieldType = 'text' | 'slug' | 'markdown' | 'image'
14
+ type FieldType = 'text' | 'slug' | 'markdown' | 'image' | 'reference'
48
15
 
49
16
  interface BaseField {
50
17
  label: string
51
18
  type: FieldType
52
19
  description?: string
20
+ presentation?: 'svg'
53
21
  }
54
22
 
23
+ // text fields
55
24
  interface TextField extends BaseField {
56
25
  type: 'text' | 'slug' | 'markdown'
57
26
  }
@@ -60,6 +29,7 @@ interface TextFieldStructure extends TextField {
60
29
  instanceOf: typeof FieldInstance
61
30
  }
62
31
 
32
+ // image field
63
33
  interface ImageField extends BaseField {
64
34
  type: 'image'
65
35
  }
@@ -68,46 +38,49 @@ interface ImageFieldStructure extends ImageField {
68
38
  instanceOf: typeof FieldInstance
69
39
  }
70
40
 
41
+ // reference fields
42
+ interface ReferenceField extends BaseField {
43
+ type: 'reference'
44
+ to: string | string[]
45
+ }
46
+
47
+ export interface ReferenceFieldStructure extends ReferenceField {
48
+ instanceOf: typeof FieldInstance
49
+ }
50
+
51
+ export type FieldDef = TextField | ImageField | ReferenceField
52
+ export type FieldDefStructure = TextFieldStructure | ImageFieldStructure | ReferenceFieldStructure
53
+
54
+ // blocks fields
71
55
  export interface BlocksFieldDef {
72
56
  label: string
73
- type: 'blocks'
74
57
  description?: string
75
- children: Record<string, BlockDefStructure | FieldDefStructure>
58
+ blocks: Record<string, BlockDefStructure | FieldDefStructure>
76
59
  }
77
60
 
78
61
  export interface BlocksFieldDefStructure extends BlocksFieldDef {
79
62
  instanceOf: typeof BlockFieldInstance
80
63
  }
81
64
 
82
- export type FieldDef = TextField | ImageField
83
- export type FieldDefStructure = TextFieldStructure | ImageFieldStructure
65
+ export type BlockFields = Record<string, FieldDefStructure | BlocksFieldDefStructure>
84
66
 
85
- export interface BlockDef {
86
- label: string
67
+ // block
68
+ export type BlockDef<T extends BlockFields> = {
87
69
  type: string
88
- fields: Record<string, FieldDefStructure | BlocksFieldDefStructure>
70
+ label: string
89
71
  description?: string
72
+ preview?: {
73
+ field: keyof T
74
+ } | {
75
+ slug: string
76
+ },
77
+ fields: T,
90
78
  }
91
79
 
92
- export interface BlockDefStructure extends BlockDef {
80
+ export type BlockDefStructure = BlockDef<BlockFields> & {
93
81
  instanceOf: typeof BlockInstance
94
82
  }
95
83
 
96
- // type DBDefaults = {
97
- // id: number
98
- // created_at: string
99
- // updated_at: string
100
- // name: string
101
- // label: string
102
- // // type: string
103
- // sort_order: number
104
- // value: string
105
- // options: string | null
106
- // status: 'enabled' | 'disabled'
107
- // parent_id: number | null
108
- // depth: number
109
- // }
110
-
111
84
  type BaseDBResult = {
112
85
  id: number
113
86
  created_at: string
@@ -117,7 +90,7 @@ type BaseDBResult = {
117
90
  sort_order: number
118
91
  value: string | null
119
92
  options: any
120
- status: 'enabled' | 'disabled'
93
+ status: BlockStatus
121
94
  parent_id: number | null
122
95
  depth: number
123
96
  }
@@ -127,31 +100,34 @@ export type DBPrimitiveFieldResult = BaseDBResult & {
127
100
  }
128
101
 
129
102
  export type DBBlockFieldResult = BaseDBResult & {
130
- type: 'blocks'
131
- children: DBBlockResult[]
103
+ blocks: DBBlockResult[]
132
104
  }
133
105
 
134
106
  export type DBBlockResult = BaseDBResult & {
135
107
  type: string
136
- fields: Record<string, DBFieldResult>
108
+ fields: Record<string, DBResult>
137
109
  }
138
110
 
139
- export type DBFieldResult = DBPrimitiveFieldResult & DBBlockFieldResult
140
-
141
- export type DBBlock = Block & {
142
- id: number
143
- created_at: string
144
- updated_at: string
145
- value: string | null
146
- sort_order: number | null
147
- parent_id: number | null
148
- options: number | null
149
- }
111
+ export type DBResult = DBPrimitiveFieldResult | DBBlockFieldResult | DBBlockResult
150
112
 
151
- export type BlockStatus = 'enabled' | 'disabled'
113
+ export type Structure = Record<string, BlockDefStructure>
152
114
 
153
115
  export type StudioConfig = {
116
+ siteName: string
117
+ admin?: {
118
+ logo?: HtmlEscapedString | Promise<HtmlEscapedString>
119
+ }
120
+ honoConfig: HonoOptions<BlankEnv>
121
+ fileBasedRouter: boolean,
122
+ port: number
123
+ structure: Structure
124
+ }
125
+
126
+ export type StudioConfigInput = {
154
127
  siteName?: string
128
+ admin?: {
129
+ logo?: HtmlEscapedString | Promise<HtmlEscapedString>
130
+ }
155
131
  honoConfig?: HonoOptions<BlankEnv>
156
132
  fileBasedRouter?: boolean,
157
133
  port?: number
package/utils/define.ts CHANGED
@@ -30,7 +30,9 @@ export function defineBlockField(
30
30
  }
31
31
  }
32
32
 
33
- export function defineBlock(block: types.BlockDef): types.BlockDefStructure {
33
+ export function defineBlock<const O extends types.BlockFields>(block: types.BlockDef<O>): types.BlockDef<O> & {
34
+ instanceOf: typeof BlockInstance;
35
+ } {
34
36
  return { ...block, instanceOf: BlockInstance }
35
37
  }
36
38
 
@@ -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
  })
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { type Context } from 'hono'
4
+ import { html } from './html.ts'
5
+
6
+ let resolvers = new Set<(value: unknown) => void>()
7
+
8
+ async function setupRefreshPromise() {
9
+ return new Promise(resolve => {
10
+ resolvers.add(resolve)
11
+ })
12
+ }
13
+
14
+ function requestRefresh() {
15
+ resolvers.forEach(resolve => resolve(true))
16
+ }
17
+
18
+ export const refresher = ({ root, exclude }: { root: string, exclude: string }) => {
19
+ const watcher = fs.watch(path.resolve(root), { recursive: true })
20
+
21
+ watcher.on('change', () => requestRefresh())
22
+
23
+ process.on('exit', () => requestRefresh())
24
+ process.on('SIGHUP', () => process.exit(128 + 1))
25
+ process.on('SIGINT', () => process.exit(128 + 2))
26
+ process.on('SIGTERM', () => process.exit(128 + 15))
27
+
28
+ return async (c: Context) => {
29
+ await setupRefreshPromise()
30
+
31
+ return c.text('')
32
+ }
33
+ }
34
+
35
+ export const refreshClient = (port: number) => html`<script defer type="module">
36
+ function reload() {
37
+ const retry = async () => {
38
+ if (await fetch('http://localhost:${port}').catch(() => false))
39
+ window.location.reload()
40
+ else requestAnimationFrame(retry)
41
+ }
42
+
43
+ retry()
44
+ }
45
+
46
+ console.log(
47
+ '%c REFRESHER ACTIVE ',
48
+ 'color: green; background: lightgreen; border-radius: 2px'
49
+ )
50
+
51
+ const response = await fetch('http://localhost:${port}/refresh').catch(
52
+ () => false
53
+ )
54
+
55
+ reload()
56
+ </script>`
@@ -4,10 +4,14 @@ import { type SSEStreamingApi } from 'hono/streaming'
4
4
  import { type Context } from 'hono'
5
5
 
6
6
  export const renderSSE = async (stream: SSEStreamingApi, c: Context) => {
7
- const componentPath = c.req.header('render')
7
+ const componentPaths = c.req.header('render')
8
8
  const props = c.req.header('props')
9
9
 
10
- if (componentPath) {
10
+ if (!componentPaths) return
11
+
12
+ for (const componentPath of componentPaths.split(' ')) {
13
+ if (!componentPath) return
14
+
11
15
  try {
12
16
  const partialToRender = await import(
13
17
  path.join('../', 'components', componentPath + '.ts')
@@ -18,8 +22,9 @@ export const renderSSE = async (stream: SSEStreamingApi, c: Context) => {
18
22
 
19
23
  await stream.writeSSE({
20
24
  event: 'datastar-patch-elements',
21
- data: `elements ${stripNewlines(component)}`,
25
+ data: component.split('\n').map((line: string) => `elements ${line}\n`).join(''),
22
26
  })
27
+
23
28
  } catch (error) {
24
29
  console.log(error)
25
30
  }
@@ -1,6 +1,6 @@
1
1
  import packageJSON from '../package.json' with { type: 'json' }
2
2
 
3
- export default ({ port, refresherPort }: { port: number, refresherPort: number }) => {
3
+ export default ({ port }: { port: number }) => {
4
4
  console.log('\x1b[32m%s\x1b[0m', '╭───────────────╮')
5
5
  console.log('\x1b[32m%s\x1b[0m', '│ Alstar Studio │')
6
6
  console.log('\x1b[32m%s\x1b[0m', '╰───────────────╯')
@@ -13,7 +13,7 @@ export default ({ port, refresherPort }: { port: number, refresherPort: number }
13
13
  console.log(' ')
14
14
  console.log('\x1b[32m%s\x1b[0m', `Studio:`)
15
15
  console.log(`http://localhost:${port}/studio`)
16
- console.log(' ')
17
- console.log('\x1b[32m%s\x1b[0m', `Refresher:`)
18
- console.log(`http://localhost:${refresherPort}`)
16
+ // console.log(' ')
17
+ // console.log('\x1b[32m%s\x1b[0m', `Refresher:`)
18
+ // console.log(`http://localhost:${refresherPort}`)
19
19
  }