@alstar/studio 0.0.0-beta.10 → 0.0.0-beta.12

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 (48) hide show
  1. package/api/api-key.ts +37 -49
  2. package/api/auth.ts +64 -0
  3. package/api/backup.ts +39 -28
  4. package/api/block.ts +80 -119
  5. package/api/index.ts +17 -10
  6. package/api/mcp.ts +39 -42
  7. package/components/AdminPanel.ts +29 -4
  8. package/components/BlockFieldRenderer.ts +20 -4
  9. package/components/Entries.ts +7 -3
  10. package/components/FieldRenderer.ts +1 -1
  11. package/components/Settings.ts +5 -93
  12. package/components/SiteLayout.ts +11 -9
  13. package/components/fields/Markdown.ts +1 -1
  14. package/components/fields/Slug.ts +122 -0
  15. package/components/fields/Text.ts +20 -4
  16. package/components/fields/index.ts +4 -1
  17. package/components/icons.ts +55 -0
  18. package/components/settings/ApiKeys.ts +122 -0
  19. package/components/settings/Backup.ts +82 -0
  20. package/components/settings/Users.ts +63 -0
  21. package/index.ts +51 -11
  22. package/package.json +6 -3
  23. package/pages/entry/[id].ts +1 -3
  24. package/pages/error.ts +14 -0
  25. package/pages/index.ts +1 -1
  26. package/pages/login.ts +21 -0
  27. package/pages/register.ts +33 -0
  28. package/pages/settings.ts +1 -3
  29. package/public/studio/css/settings.css +7 -15
  30. package/public/studio/js/markdown-editor.js +1 -1
  31. package/public/studio/js/sortable-list.js +1 -1
  32. package/public/studio/main.css +5 -1
  33. package/public/studio/main.js +13 -0
  34. package/queries/block-2.ts +339 -0
  35. package/queries/getBlockTrees-2.ts +71 -0
  36. package/queries/index.ts +1 -1
  37. package/readme.md +2 -2
  38. package/schema.sql +18 -0
  39. package/schemas.ts +11 -1
  40. package/types.ts +11 -0
  41. package/utils/auth.ts +54 -0
  42. package/utils/create-hash.ts +9 -0
  43. package/utils/define.ts +1 -3
  44. package/utils/renderSSE.ts +27 -0
  45. package/utils/slugify.ts +3 -1
  46. package/utils/startup-log.ts +15 -6
  47. package/components/Backup.ts +0 -10
  48. package/utils/build-structure-path.ts +0 -43
@@ -0,0 +1,339 @@
1
+ import { db } from '@alstar/db'
2
+ import { sql } from '../utils/sql.ts'
3
+ import { type DBBlockResult } from '../types.ts'
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
+ }
83
+
84
+ if (!isBlocksChild) {
85
+ delete block.children
86
+ } else {
87
+ delete block.fields
88
+ }
89
+
90
+ return block
91
+ }
92
+
93
+ function transformForest(blocks: DBBlockResult[]): DBBlockResult[] {
94
+ return blocks.map((block) => transformBlocksTree(block))
95
+ }
96
+
97
+ function rootQuery(filterSql: string, depthLimit?: number) {
98
+ const depthLimitClause =
99
+ depthLimit !== undefined ? `WHERE d.depth + 1 <= ${depthLimit}` : ''
100
+
101
+ return sql`
102
+ with recursive
103
+ ancestors as (
104
+ select
105
+ id,
106
+ created_at,
107
+ updated_at,
108
+ name,
109
+ label,
110
+ type,
111
+ sort_order,
112
+ value,
113
+ options,
114
+ status,
115
+ parent_id,
116
+ 0 as depth
117
+ from
118
+ blocks
119
+ where
120
+ ${filterSql}
121
+ union all
122
+ select
123
+ b.id,
124
+ b.created_at,
125
+ b.updated_at,
126
+ b.name,
127
+ b.label,
128
+ b.type,
129
+ b.sort_order,
130
+ b.value,
131
+ b.options,
132
+ b.status,
133
+ b.parent_id,
134
+ a.depth + 1
135
+ from
136
+ blocks b
137
+ inner join ancestors a on b.id = a.parent_id
138
+ ),
139
+ roots as (
140
+ select
141
+ id,
142
+ created_at,
143
+ updated_at,
144
+ name,
145
+ label,
146
+ type,
147
+ sort_order,
148
+ value,
149
+ options,
150
+ status,
151
+ parent_id,
152
+ 0 as depth
153
+ from
154
+ ancestors
155
+ where
156
+ parent_id is null
157
+ ),
158
+ descendants as (
159
+ select
160
+ id,
161
+ created_at,
162
+ updated_at,
163
+ name,
164
+ label,
165
+ type,
166
+ sort_order,
167
+ value,
168
+ options,
169
+ status,
170
+ parent_id,
171
+ depth
172
+ from
173
+ roots
174
+ union all
175
+ select
176
+ b.id,
177
+ b.created_at,
178
+ b.updated_at,
179
+ b.name,
180
+ b.label,
181
+ b.type,
182
+ b.sort_order,
183
+ b.value,
184
+ b.options,
185
+ b.status,
186
+ b.parent_id,
187
+ d.depth + 1
188
+ from
189
+ blocks b
190
+ inner join descendants d on b.parent_id = d.id ${depthLimitClause}
191
+ )
192
+ select
193
+ *
194
+ from
195
+ descendants
196
+ order by
197
+ sort_order,
198
+ id;
199
+ `
200
+ }
201
+
202
+ function buildFilterSql(params: Record<string, any>) {
203
+ const entries = Object.entries(params)
204
+ const filterSql = entries
205
+ .map(([key, value]) =>
206
+ value === null ? `${key} is null` : `${key} = :${key}`,
207
+ )
208
+ .join(' and ')
209
+
210
+ let sqlParams: Record<keyof typeof params, any> = {}
211
+
212
+ for (const param in params) {
213
+ if (params[param] !== null) {
214
+ sqlParams[param] = params[param]
215
+ }
216
+ }
217
+
218
+ return { filterSql, sqlParams }
219
+ }
220
+
221
+ export function roots(
222
+ params: Record<string, any>,
223
+ options?: {
224
+ depth?: number
225
+ },
226
+ ): DBBlockResult[] | [] {
227
+ const { filterSql, sqlParams } = buildFilterSql(params)
228
+
229
+ const query = rootQuery(filterSql, options?.depth)
230
+ const rows = db.database
231
+ .prepare(query)
232
+ .all(sqlParams) as unknown as DBBlockResult[]
233
+
234
+ if (!rows.length) return []
235
+
236
+ const forest = buildForest(rows)
237
+
238
+ return transformForest(forest)
239
+ }
240
+
241
+ type DBRow = {
242
+ id: number
243
+ created_at: string
244
+ updated_at: string
245
+ name: string
246
+ label: string
247
+ type: string
248
+ sort_order: number
249
+ value: string | null
250
+ options: string | null
251
+ status: 'enabled' | 'disabled'
252
+ parent_id: number | null
253
+ depth: number
254
+ }
255
+
256
+ type TODO = any
257
+
258
+ function buildTree2(items: DBRow[]): TODO | null {
259
+ const map = new Map<number, TODO>();
260
+
261
+ // First pass: clone items into map
262
+ for (const item of items) {
263
+ map.set(item.id, { ...item });
264
+ }
265
+
266
+ let root: TODO | null = null;
267
+
268
+ // Second pass: assign children to parents
269
+ for (const item of map.values()) {
270
+ if (item.parent_id === null) {
271
+ root = item; // Root node
272
+ } else {
273
+ const parent = map.get(item.parent_id);
274
+ if (parent) {
275
+ if(parent.type === 'blocks') {
276
+ if (!parent.children) parent.children = [];
277
+ parent.children.push(item);
278
+ } else {
279
+ if (!parent.fields) parent.fields = {};
280
+ parent.fields[item.name] = item;
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ return root;
287
+ }
288
+
289
+ export function root(
290
+ params: Record<string, any>,
291
+ options?: { depth?: number },
292
+ ): DBBlockResult | null {
293
+ const { filterSql, sqlParams } = buildFilterSql(params)
294
+
295
+ const query = rootQuery(filterSql, options?.depth)
296
+ const rows = db.database
297
+ .prepare(query)
298
+ .all(sqlParams) as unknown as DBRow[]
299
+
300
+ if (!rows.length) return null
301
+
302
+ // const tree = buildTree(rows)
303
+
304
+ const tree = buildTree2(rows)
305
+
306
+ return tree // transformBlocksTree(tree)
307
+ }
308
+
309
+ export function block(params: Record<string, any>) {
310
+ const { filterSql, sqlParams } = buildFilterSql(params)
311
+
312
+ const query = sql`
313
+ select
314
+ *
315
+ from
316
+ blocks
317
+ where
318
+ ${filterSql}
319
+ `
320
+
321
+ return db.database.prepare(query).get(sqlParams) as unknown as DBBlockResult
322
+ }
323
+
324
+ export function blocks(params: Record<string, any>) {
325
+ const { filterSql, sqlParams } = buildFilterSql(params)
326
+
327
+ const query = sql`
328
+ select
329
+ *
330
+ from
331
+ blocks
332
+ where
333
+ ${filterSql}
334
+ order by
335
+ sort_order
336
+ `
337
+
338
+ return db.database.prepare(query).all(sqlParams) as unknown as DBBlockResult[]
339
+ }
@@ -0,0 +1,71 @@
1
+ import { structure } from "../index.ts";
2
+
3
+ export const defineField = <
4
+ F extends Record<string, any> | undefined = undefined,
5
+ C extends Record<string, any> | undefined = undefined
6
+ >(config: {
7
+ type: string;
8
+ label: string;
9
+ fields?: F;
10
+ children?: C;
11
+ }) =>
12
+ ({
13
+ kind: 'field',
14
+ type: config.type,
15
+ label: config.label,
16
+ fields: (config.fields ?? {}) as F,
17
+ children: (config.children ?? {}) as C,
18
+ } as const);
19
+
20
+ export const defineBlock = <
21
+ F extends Record<string, any> = {},
22
+ C extends Record<string, any> = {}
23
+ >(config: {
24
+ type: string;
25
+ label: string;
26
+ fields?: F;
27
+ children?: C;
28
+ }) =>
29
+ ({
30
+ kind: 'block',
31
+ type: config.type,
32
+ label: config.label,
33
+ fields: (config.fields ?? {}) as F,
34
+ children: (config.children ?? {}) as C,
35
+ } as const);
36
+
37
+ type InferBlock<T> =
38
+ // A Block has fields + children
39
+ T extends { kind: 'block'; fields: infer F; children: infer C }
40
+ ? {
41
+ fields: { [K in keyof F]: InferBlock<F[K]> };
42
+ children: { [K in keyof C]: InferBlock<C[K]> }[keyof C][];
43
+ }
44
+ // A Field without children
45
+ : T extends { kind: 'field'; children: infer C; fields: infer F }
46
+ ? keyof C extends never
47
+ ? { value: string | null; type: string; label: string }
48
+ // A Field with children (like "blocks")
49
+ : {
50
+ fields: { [K in keyof F]: InferBlock<F[K]> };
51
+ children: { [K in keyof C]: InferBlock<C[K]> }[keyof C][];
52
+ type: string;
53
+ label: string;
54
+ }
55
+ : never;
56
+
57
+ // Top-level inference for the structure object
58
+ type InferStructure<S> = {
59
+ [K in keyof S]: InferBlock<S[K]>;
60
+ };
61
+
62
+ type AppStructure = typeof structure;
63
+ type AppBlocks = InferStructure<AppStructure>;
64
+
65
+ // Example: AppBlocks["page"] is the typed shape for a page block
66
+ export function getBlockTrees<T extends keyof AppBlocks>(
67
+ rootType: T
68
+ ): AppBlocks[T][] {
69
+ // Your SQL + builder code goes here
70
+ return [] as any;
71
+ }
package/queries/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * as query from './block.ts'
1
+ export * as query from './block-2.ts'
2
2
  export { getBlockTrees } from './getBlockTrees.ts'
package/readme.md CHANGED
@@ -45,7 +45,7 @@ Pages are defined in the `/pages` directory.
45
45
  Access the CMS at:
46
46
 
47
47
  ```
48
- /admin
48
+ /studio
49
49
  ```
50
50
 
51
51
  ### Defining Content Structure
@@ -199,7 +199,7 @@ pnpm run dev
199
199
 
200
200
  Visit:
201
201
 
202
- * **CMS admin**: `http://localhost:3000/admin`
202
+ * **CMS admin**: `http://localhost:3000/studio`
203
203
  * **Frontend page**: `http://localhost:3000/my-first-page`
204
204
 
205
205
  Create a new page in the CMS, set its slug field to `my-first-page`, and the frontend will render it automatically.
package/schema.sql ADDED
@@ -0,0 +1,18 @@
1
+ CREATE TABLE blocks (
2
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3
+ created_at DATE DEFAULT (datetime('now')),
4
+ updated_at DATE DEFAULT (datetime('now')),
5
+
6
+ name TEXT not null,
7
+ label TEXT not null,
8
+ type TEXT not null,
9
+ sort_order INTEGER not null default 0,
10
+ value TEXT,
11
+ options JSON,
12
+ status TEXT default 'enabled',
13
+ parent_id INTEGER,
14
+ _depth INTEGER,
15
+ foreign key (parent_id) references blocks (id)
16
+
17
+ );
18
+ CREATE TABLE sqlite_sequence(name,seq);
package/schemas.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { sql } from './utils/sql.ts'
2
2
 
3
+ // -- API keys
4
+ export const usersTable = {
5
+ tableName: 'users',
6
+ columns: sql`
7
+ email TEXT not null,
8
+ hash TEXT
9
+ `,
10
+ }
11
+
12
+
3
13
  // -- Blocks
4
14
  export const blocksTable = {
5
15
  tableName: 'blocks',
@@ -11,7 +21,7 @@ export const blocksTable = {
11
21
  options JSON,
12
22
  status TEXT default 'enabled',
13
23
  sort_order INTEGER not null default 0,
14
- _depth INTEGER,
24
+ -- _depth INTEGER,
15
25
  parent_id INTEGER,
16
26
  foreign key (parent_id) references blocks (id)
17
27
  `,
package/types.ts CHANGED
@@ -8,6 +8,16 @@ import {
8
8
  FieldInstance,
9
9
  } from './utils/define.ts'
10
10
 
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
+
11
21
  export type PrimitiveField = {
12
22
  name: string
13
23
  label: string
@@ -31,6 +41,7 @@ export type Block = {
31
41
  }
32
42
 
33
43
  export type Structure = Record<string, BlockDefStructure>
44
+ // export type Structure = Record<string, BlockDefStructure>
34
45
 
35
46
  // --- Field & block definitions ---
36
47
  type FieldType = 'text' | 'slug' | 'markdown' | 'image'
package/utils/auth.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { db } from '@alstar/db'
2
+ import { type MiddlewareHandler } from 'hono'
3
+ import { getCookie, setCookie } from 'hono/cookie'
4
+ import { HTTPException } from 'hono/http-exception'
5
+ import { sql } from './sql.ts'
6
+
7
+ const middleware: MiddlewareHandler = async (c, next) => {
8
+ const url = new URL(c.req.url)
9
+ const user = db.database
10
+ .prepare(sql`
11
+ select
12
+ email
13
+ from
14
+ users
15
+ `)
16
+ .get()
17
+
18
+ const cookie = getCookie(c, 'login')
19
+
20
+ if (
21
+ !user &&
22
+ url.pathname !== '/studio/register' &&
23
+ url.pathname !== '/studio/api/auth/register'
24
+ ) {
25
+ return c.redirect('/studio/register')
26
+ }
27
+
28
+ if (
29
+ user &&
30
+ !cookie &&
31
+ url.pathname !== '/studio/login' &&
32
+ url.pathname !== '/studio/api/auth/login'
33
+ ) {
34
+ return c.redirect('/studio/login')
35
+ }
36
+
37
+ // console.log(cookie)
38
+
39
+ // deleteCookie(c, 'cookie_name')
40
+
41
+ // const allCookies = getCookie(c)
42
+
43
+ await next()
44
+
45
+ // const authorized = false
46
+
47
+ // if(!authorized) {
48
+ // throw new HTTPException(401, { message: 'Custom error message' })
49
+ // } else {
50
+ // await next()
51
+ // }
52
+ }
53
+
54
+ export default middleware
@@ -0,0 +1,9 @@
1
+ import crypto from 'node:crypto'
2
+
3
+ export const createHash = (str: string) => {
4
+ const hash = crypto.createHash('sha256')
5
+
6
+ hash.update(str)
7
+
8
+ return hash.digest().toString('base64')
9
+ }
package/utils/define.ts CHANGED
@@ -34,8 +34,6 @@ export function defineBlock(block: types.BlockDef): types.BlockDefStructure {
34
34
  return { ...block, instanceOf: BlockInstance }
35
35
  }
36
36
 
37
- export function defineStructure(
38
- structure: Record<string, types.BlockDefStructure>,
39
- ) {
37
+ export function defineStructure(structure: types.Structure) {
40
38
  return structure
41
39
  }
@@ -0,0 +1,27 @@
1
+ import path from 'node:path'
2
+ import { stripNewlines } from './strip-newlines.ts'
3
+ import { type SSEStreamingApi } from 'hono/streaming'
4
+ import { type Context } from 'hono'
5
+
6
+ export const renderSSE = async (stream: SSEStreamingApi, c: Context) => {
7
+ const componentPath = c.req.header('render')
8
+ const props = c.req.header('props')
9
+
10
+ if (componentPath) {
11
+ try {
12
+ const partialToRender = await import(
13
+ path.join('../', 'components', componentPath + '.ts')
14
+ )
15
+
16
+ const propsJSON = props ? JSON.parse(props) : undefined
17
+ const component = await partialToRender.default(propsJSON)
18
+
19
+ await stream.writeSSE({
20
+ event: 'datastar-patch-elements',
21
+ data: `elements ${stripNewlines(component)}`,
22
+ })
23
+ } catch (error) {
24
+ console.log(error)
25
+ }
26
+ }
27
+ }
package/utils/slugify.ts CHANGED
@@ -1,4 +1,6 @@
1
- export function slugify(str: string) {
1
+ export function slugify(str?: string | null) {
2
+ if (!str) return ''
3
+
2
4
  return String(str)
3
5
  .normalize('NFKD') // split accented characters into their base characters and diacritical marks
4
6
  .replace(/[\u0300-\u036f]/g, '') // remove all the accents, which happen to be all in the \u03xx UNICODE block.
@@ -1,10 +1,19 @@
1
1
  import packageJSON from '../package.json' with { type: 'json' }
2
2
 
3
3
  export default ({ port, refresherPort }: { port: number, refresherPort: number }) => {
4
- console.log('\x1b[32m%s\x1b[0m', '╭───────────────────────╮')
5
- console.log('\x1b[32m%s\x1b[0m', '│ Alstar Studio │')
6
- console.log('\x1b[32m%s\x1b[0m', `│ ${packageJSON.version}${' '.repeat(22 - packageJSON.version.length)}│`)
7
- console.log('\x1b[32m%s\x1b[0m', `│ http://localhost:${port} │`)
8
- console.log('\x1b[32m%s\x1b[0m', `│ Refresher port: ${refresherPort} │`)
9
- console.log('\x1b[32m%s\x1b[0m', '╰───────────────────────╯')
4
+ console.log('\x1b[32m%s\x1b[0m', '╭───────────────╮')
5
+ console.log('\x1b[32m%s\x1b[0m', '│ Alstar Studio │')
6
+ console.log('\x1b[32m%s\x1b[0m', '╰───────────────╯')
7
+ console.log(' ')
8
+ console.log('\x1b[32m%s\x1b[0m', `Version:`)
9
+ console.log(packageJSON.version)
10
+ console.log(' ')
11
+ console.log('\x1b[32m%s\x1b[0m', `App:`)
12
+ console.log(`http://localhost:${port}`)
13
+ console.log(' ')
14
+ console.log('\x1b[32m%s\x1b[0m', `Studio:`)
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}`)
10
19
  }
@@ -1,10 +0,0 @@
1
- import { html } from 'hono/html'
2
-
3
- export default () => {
4
- return html`<article>
5
- <header>Backup</header>
6
- <form data-on-submit="@post('/admin/api/backup', { contentType: 'form' })">
7
- <button type="submit">Backup database</button>
8
- </form>
9
- </article>`
10
- }
@@ -1,43 +0,0 @@
1
- import { type Block } from '../types.ts'
2
- import { structure } from '../index.ts'
3
-
4
- export type StructurePath = (string | number)[]
5
-
6
- function getTargetPath(
7
- target: Block,
8
- path: StructurePath,
9
- ): number | string | undefined {
10
- if (!path.length) {
11
- return structure.findIndex((block) => block.name === target.name)
12
- }
13
-
14
- let sub = structure
15
-
16
- path.forEach((key: number | string) => {
17
- if (sub) {
18
- // @ts-ignore
19
- sub = sub[key]
20
- }
21
- })
22
-
23
- if (Array.isArray(sub)) {
24
- return sub.findIndex((block) => block.name === target.name)
25
- }
26
-
27
- return
28
- }
29
-
30
- export const buildStructurePath = (
31
- target: Block | undefined,
32
- path: StructurePath = [],
33
- ) => {
34
- if (!target) return path
35
-
36
- const targetPlacement = getTargetPath(target, path)
37
-
38
- if (targetPlacement !== undefined) {
39
- path.push(targetPlacement)
40
- }
41
-
42
- return path
43
- }