create-kywi-app 0.5.0 → 0.6.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.
- package/lib/templates.mjs +193 -14
- package/package.json +1 -1
package/lib/templates.mjs
CHANGED
|
@@ -383,12 +383,13 @@ import type {
|
|
|
383
383
|
ModuleNode,
|
|
384
384
|
FeedItemsResolver,
|
|
385
385
|
ComponentResolver,
|
|
386
|
+
SectionComponentResolver,
|
|
386
387
|
PersonalizationState,
|
|
387
388
|
} from '@kywi-software/core/layout'
|
|
388
389
|
import { isLayoutSection, applyPageVariant } from '@kywi-software/core/layout'
|
|
389
390
|
import { resolveContentByPath, normalizePath } from '@kywi-software/core/nav'
|
|
390
391
|
import { getFeedBySlug, getComponentById, resolveLocaleFromRequest } from '@kywi-software/core'
|
|
391
|
-
import { resolveComponentDefinition } from '@kywi-software/core/admin/server'
|
|
392
|
+
import { resolveComponentDefinition, resolveSectionComponentDefinition } from '@kywi-software/core/admin/server'
|
|
392
393
|
import {
|
|
393
394
|
evaluateActiveAudiences,
|
|
394
395
|
getSelfIdWidgetConfig,
|
|
@@ -590,6 +591,41 @@ export async function buildComponentResolver(
|
|
|
590
591
|
return (componentId) => resolveComponentDefinition(map.get(componentId) ?? null)
|
|
591
592
|
}
|
|
592
593
|
|
|
594
|
+
/** Yield every section-level component reference (\`section.componentId\`) in the
|
|
595
|
+
* layout, descending into variantContainer default + variant sections (#69). */
|
|
596
|
+
function* sectionComponentIds(regions: Record<string, RegionNode[]>): Generator<string> {
|
|
597
|
+
for (const nodes of Object.values(regions)) {
|
|
598
|
+
for (const node of nodes) {
|
|
599
|
+
if (isLayoutSection(node)) {
|
|
600
|
+
if (node.componentId) yield node.componentId
|
|
601
|
+
} else {
|
|
602
|
+
for (const s of node.defaultSections) if (s.componentId) yield s.componentId
|
|
603
|
+
for (const v of node.variants) for (const s of v.sections) if (s.componentId) yield s.componentId
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Prefetch every connected *section* component (\`section.componentId\`) referenced
|
|
611
|
+
* by the layout and return a synchronous {@link SectionComponentResolver} over the
|
|
612
|
+
* snapshot — parallel to {@link buildComponentResolver} but for reusable sections
|
|
613
|
+
* (#69). Returns undefined when the layout references none (skip the prop).
|
|
614
|
+
*/
|
|
615
|
+
export async function buildSectionComponentResolver(
|
|
616
|
+
layout: LayoutDocument,
|
|
617
|
+
runtime: KywiRuntime,
|
|
618
|
+
): Promise<SectionComponentResolver | undefined> {
|
|
619
|
+
const ids = new Set<string>(sectionComponentIds(layout.regions))
|
|
620
|
+
if (ids.size === 0) return undefined
|
|
621
|
+
const { db, siteId } = runtime
|
|
622
|
+
const entries = await Promise.all(
|
|
623
|
+
[...ids].map(async (id) => [id, await getComponentById(db, id, siteId)] as const),
|
|
624
|
+
)
|
|
625
|
+
const map = new Map(entries)
|
|
626
|
+
return (componentId) => resolveSectionComponentDefinition(map.get(componentId) ?? null)
|
|
627
|
+
}
|
|
628
|
+
|
|
593
629
|
// ─── Personalization + experiments (#50) ─────────────────────────────────────
|
|
594
630
|
|
|
595
631
|
/** Server-resolved personalization for one public request. */
|
|
@@ -792,6 +828,10 @@ import {
|
|
|
792
828
|
setAccessCookie,
|
|
793
829
|
clearSessionCookies,
|
|
794
830
|
} from '@kywi-software/core/host'
|
|
831
|
+
// Edge-safe leaf helper: builds the kywi_utm cookie value core's collectUtm reads
|
|
832
|
+
// back. Its own dependency-free entry (never the DB-backed audiences barrel), so
|
|
833
|
+
// it is importable from this edge middleware.
|
|
834
|
+
import { UTM_COOKIE, utmCookieValue } from '@kywi-software/core/audiences/utm-persistence'
|
|
795
835
|
|
|
796
836
|
/**
|
|
797
837
|
* Server-side auth enforcement + transparent session refresh for /admin and the
|
|
@@ -838,17 +878,30 @@ const VISITOR_MAX_AGE = 60 * 60 * 24 * 365 * 2 // 2 years
|
|
|
838
878
|
* Public (non-admin, non-API) request: never auth-gated. Ensure a stable
|
|
839
879
|
* \`kywi_visitor\` id — mint one on first visit — and forward it to the render as
|
|
840
880
|
* \`x-kywi-visitor\` so the page sees it on this very request (the freshly set
|
|
841
|
-
* cookie is not yet readable via cookies()).
|
|
881
|
+
* cookie is not yet readable via cookies()). Also forward the full request URL
|
|
882
|
+
* as \`x-kywi-url\` (query string included) so the server render can read UTM
|
|
883
|
+
* params off the very first request — without it, campaign personalization
|
|
884
|
+
* would never fire on the landing page. Everything else passes through.
|
|
842
885
|
*/
|
|
843
886
|
function handlePublicRequest(req: NextRequest): NextResponse {
|
|
844
887
|
const existing = req.cookies.get(VISITOR_COOKIE)?.value
|
|
845
888
|
const visitorId = existing ?? crypto.randomUUID()
|
|
846
889
|
const headers = new Headers(req.headers)
|
|
847
890
|
headers.set('x-kywi-visitor', visitorId)
|
|
891
|
+
headers.set('x-kywi-url', req.nextUrl.href)
|
|
848
892
|
const res = NextResponse.next({ request: { headers } })
|
|
849
893
|
if (!existing) {
|
|
850
894
|
res.cookies.set(VISITOR_COOKIE, visitorId, { path: '/', maxAge: VISITOR_MAX_AGE, sameSite: 'lax' })
|
|
851
895
|
}
|
|
896
|
+
// Persist campaign UTM params so audience matching survives internal
|
|
897
|
+
// navigation: core's collectUtm reads this cookie when the URL has no utm_*
|
|
898
|
+
// query string. Only written when the request carries utm_* params, so an
|
|
899
|
+
// ordinary page view never clobbers a persisted campaign (utmCookieValue
|
|
900
|
+
// returns null → the existing cookie is left in place).
|
|
901
|
+
const utmValue = utmCookieValue(req.nextUrl.href)
|
|
902
|
+
if (utmValue) {
|
|
903
|
+
res.cookies.set(UTM_COOKIE, utmValue, { path: '/', maxAge: 60 * 60 * 24 * 30, sameSite: 'lax' })
|
|
904
|
+
}
|
|
852
905
|
return res
|
|
853
906
|
}
|
|
854
907
|
|
|
@@ -1117,6 +1170,7 @@ import {
|
|
|
1117
1170
|
clientRuntimeEnabled,
|
|
1118
1171
|
buildFeedResolver,
|
|
1119
1172
|
buildComponentResolver,
|
|
1173
|
+
buildSectionComponentResolver,
|
|
1120
1174
|
localeAlternates,
|
|
1121
1175
|
mediaUrl,
|
|
1122
1176
|
requestBaseUrl,
|
|
@@ -1255,6 +1309,7 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1255
1309
|
const personalized = personalizeLayout(layout, perso.audienceId)
|
|
1256
1310
|
const hydrated = await hydrateLayoutFeeds(personalized, buildFeedResolver(runtime))
|
|
1257
1311
|
const componentResolver = await buildComponentResolver(hydrated, runtime)
|
|
1312
|
+
const sectionComponentResolver = await buildSectionComponentResolver(hydrated, runtime)
|
|
1258
1313
|
content = (
|
|
1259
1314
|
<article className="page page--layout" data-kywi-content-id={contentId}>
|
|
1260
1315
|
{head}
|
|
@@ -1263,6 +1318,7 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1263
1318
|
personalization={perso.personalization}
|
|
1264
1319
|
moduleComponents={moduleComponents}
|
|
1265
1320
|
{...(componentResolver ? { componentResolver } : {})}
|
|
1321
|
+
{...(sectionComponentResolver ? { sectionComponentResolver } : {})}
|
|
1266
1322
|
>
|
|
1267
1323
|
{Object.keys(hydrated.regions).map((name) => (
|
|
1268
1324
|
<KywiRegion key={name} name={name} />
|
|
@@ -1298,9 +1354,12 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1298
1354
|
canEdit={perms.canEdit}
|
|
1299
1355
|
canPublish={perms.canPublish}
|
|
1300
1356
|
contentId={contentId}
|
|
1357
|
+
contentType={contentType}
|
|
1301
1358
|
pageTitle={title}
|
|
1302
1359
|
pageStatus={String(node['status'] ?? '')}
|
|
1360
|
+
initialLayout={layout ?? { regions: { main: [] } }}
|
|
1303
1361
|
adminHref={\`/admin/content/\${contentType}/\${contentId}\`}
|
|
1362
|
+
moduleComponents={moduleComponents}
|
|
1304
1363
|
>
|
|
1305
1364
|
{content}
|
|
1306
1365
|
</KywiFrontEdit>
|
|
@@ -1315,56 +1374,167 @@ export default async function PublicPage({ params, searchParams }: Params & Sear
|
|
|
1315
1374
|
/**
|
|
1316
1375
|
* Front-of-site edit overlay (client). Mounted by the public page ONLY for an
|
|
1317
1376
|
* authenticated admin who opened the page with ?kywi-edit=1 (the page verifies
|
|
1318
|
-
* the session cookie server-side first).
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1377
|
+
* the session cookie server-side first).
|
|
1378
|
+
*
|
|
1379
|
+
* Two modes, same component:
|
|
1380
|
+
* - **Browse** — core's slim `KywiEditToolbar` over the live page, with the
|
|
1381
|
+
* editable regions outlined via the `data-kywi-*` protocol.
|
|
1382
|
+
* - **Edit** (`?kywi-edit=1`, or the toolbar's Edit toggle) — the FULL
|
|
1383
|
+
* `OverlayShell` layout editor, the same one the admin app mounts, rendered
|
|
1384
|
+
* from the same core module registry + custom renderers. Save PUTs the layout
|
|
1385
|
+
* document; Publish PUTs the layout then flips status — exactly the API the
|
|
1386
|
+
* admin editor uses.
|
|
1387
|
+
*
|
|
1388
|
+
* The OverlayShell is lazy-loaded (`next/dynamic`, client-only): its weight
|
|
1389
|
+
* (dnd-kit, canvas, side panels) is only fetched when an editor actually enters
|
|
1390
|
+
* edit mode, so the public browse bundle stays light.
|
|
1323
1391
|
*/
|
|
1324
1392
|
function frontEditOverlay() {
|
|
1325
1393
|
return `'use client'
|
|
1326
1394
|
import React from 'react'
|
|
1395
|
+
import dynamic from 'next/dynamic'
|
|
1327
1396
|
import { KywiEditToolbar, useKywiEditMode } from '@kywi-software/core/scope-client'
|
|
1328
1397
|
import { adminFetch } from '@kywi-software/core/host-client'
|
|
1398
|
+
import {
|
|
1399
|
+
createModuleRegistry,
|
|
1400
|
+
createThemeRegistry,
|
|
1401
|
+
BUILT_IN_MODULE_COMPONENTS,
|
|
1402
|
+
type LayoutDocument,
|
|
1403
|
+
type ModuleComponentMap,
|
|
1404
|
+
} from '@kywi-software/core/layout'
|
|
1405
|
+
import type { SaveAction } from '@kywi-software/core/admin'
|
|
1406
|
+
|
|
1407
|
+
// Lazy-load the full layout editor AND its stylesheet: the shell bundle (dnd-kit,
|
|
1408
|
+
// canvas, panels) and the ~4.7k-line admin design system are pulled INSIDE this
|
|
1409
|
+
// factory, so they enter the module graph only when an editor opens the overlay —
|
|
1410
|
+
// never in the public browse bundle a visitor downloads.
|
|
1411
|
+
const OverlayShell = dynamic(
|
|
1412
|
+
async () => {
|
|
1413
|
+
await import('@kywi-software/core/admin/styles.css')
|
|
1414
|
+
const mod = await import('@kywi-software/core/admin')
|
|
1415
|
+
return mod.OverlayShell
|
|
1416
|
+
},
|
|
1417
|
+
{ ssr: false },
|
|
1418
|
+
)
|
|
1329
1419
|
|
|
1330
1420
|
export interface KywiFrontEditProps {
|
|
1331
1421
|
canEdit: boolean
|
|
1332
1422
|
canPublish: boolean
|
|
1333
1423
|
contentId: string
|
|
1424
|
+
contentType: string
|
|
1334
1425
|
pageTitle: string
|
|
1335
1426
|
pageStatus: string
|
|
1427
|
+
/** The page's saved layout document (empty regions when it has none yet). */
|
|
1428
|
+
initialLayout: LayoutDocument
|
|
1336
1429
|
/** Deep link to this page in the full admin editor. */
|
|
1337
1430
|
adminHref: string
|
|
1431
|
+
/** Custom (defineModule) renderers, shared with the public layout + admin (#48). */
|
|
1432
|
+
moduleComponents?: ModuleComponentMap
|
|
1338
1433
|
children: React.ReactNode
|
|
1339
1434
|
}
|
|
1340
1435
|
|
|
1341
1436
|
/**
|
|
1342
1437
|
* Wraps the public page with the front-of-site edit affordance. The page only
|
|
1343
1438
|
* renders this for a signed-in admin who asked to edit (?kywi-edit=1), so the
|
|
1344
|
-
*
|
|
1439
|
+
* permission gate (canEdit) is always satisfied here.
|
|
1345
1440
|
*/
|
|
1346
1441
|
export function KywiFrontEdit({
|
|
1347
1442
|
canEdit,
|
|
1348
1443
|
canPublish,
|
|
1349
1444
|
contentId,
|
|
1445
|
+
contentType,
|
|
1350
1446
|
pageTitle,
|
|
1351
1447
|
pageStatus,
|
|
1448
|
+
initialLayout,
|
|
1352
1449
|
adminHref,
|
|
1450
|
+
moduleComponents = {},
|
|
1353
1451
|
children,
|
|
1354
1452
|
}: KywiFrontEditProps) {
|
|
1355
1453
|
const edit = useKywiEditMode({ canEdit, canPublish })
|
|
1356
1454
|
|
|
1357
1455
|
// ?kywi-edit=1 means "enter edit mode now" — flip it on once after mount.
|
|
1358
|
-
const { startEdit } = edit
|
|
1456
|
+
const { startEdit, endEdit } = edit
|
|
1359
1457
|
React.useEffect(() => {
|
|
1360
1458
|
startEdit()
|
|
1361
1459
|
}, [startEdit])
|
|
1362
1460
|
|
|
1363
|
-
|
|
1461
|
+
// Registries + renderers for the editor: built from core (no module list is
|
|
1462
|
+
// re-declared here) and merged with this app's custom module renderers from
|
|
1463
|
+
// lib/modules — the SAME source the admin app and public renderer use (#48).
|
|
1464
|
+
const moduleRegistry = React.useMemo(() => createModuleRegistry([]), [])
|
|
1465
|
+
const themeRegistry = React.useMemo(() => createThemeRegistry(), [])
|
|
1466
|
+
const editorComponents = React.useMemo(
|
|
1467
|
+
() => ({ ...BUILT_IN_MODULE_COMPONENTS, ...moduleComponents }),
|
|
1468
|
+
[moduleComponents],
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
// Persist the edited layout to the same content API the admin editor uses.
|
|
1472
|
+
const persistLayout = React.useCallback(
|
|
1473
|
+
async (next: LayoutDocument) => {
|
|
1474
|
+
const res = await fetch(\`/api/v1/content/\${contentType}/\${contentId}/layout\`, {
|
|
1475
|
+
method: 'PUT',
|
|
1476
|
+
credentials: 'same-origin',
|
|
1477
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1478
|
+
body: JSON.stringify(next),
|
|
1479
|
+
})
|
|
1480
|
+
return res.ok
|
|
1481
|
+
},
|
|
1482
|
+
[contentType, contentId],
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
const handleSave = React.useCallback(
|
|
1486
|
+
async (next: LayoutDocument, _action: SaveAction) => {
|
|
1487
|
+
await persistLayout(next)
|
|
1488
|
+
endEdit()
|
|
1489
|
+
},
|
|
1490
|
+
[persistLayout, endEdit],
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
const handlePublish = React.useCallback(
|
|
1494
|
+
async (next: LayoutDocument, _action: SaveAction) => {
|
|
1495
|
+
const ok = await persistLayout(next)
|
|
1496
|
+
if (ok) {
|
|
1497
|
+
// Best-effort status flip; the layout itself is already persisted above.
|
|
1498
|
+
await fetch(\`/api/v1/content/\${contentType}/\${contentId}\`, {
|
|
1499
|
+
method: 'PUT',
|
|
1500
|
+
credentials: 'same-origin',
|
|
1501
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1502
|
+
body: JSON.stringify({ status: 'published' }),
|
|
1503
|
+
}).catch(() => undefined)
|
|
1504
|
+
}
|
|
1505
|
+
endEdit()
|
|
1506
|
+
},
|
|
1507
|
+
[persistLayout, contentType, contentId, endEdit],
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1510
|
+
// Browse-mode one-click publish from the toolbar (no editor needed).
|
|
1511
|
+
const handleToolbarPublish = React.useCallback(async () => {
|
|
1364
1512
|
const res = await adminFetch(\`/api/v1/content/by-id/\${contentId}/publish\`, { method: 'POST' })
|
|
1365
1513
|
if (res.ok) window.location.reload()
|
|
1366
1514
|
}, [contentId])
|
|
1367
1515
|
|
|
1516
|
+
// Edit mode: the full layout editor, in place, over the live page.
|
|
1517
|
+
if (edit.isEditMode && edit.canEdit) {
|
|
1518
|
+
return (
|
|
1519
|
+
<div className="kywi-admin-shell kywi-frontend-edit">
|
|
1520
|
+
<OverlayShell
|
|
1521
|
+
editMode={edit}
|
|
1522
|
+
initialLayout={initialLayout}
|
|
1523
|
+
contentId={contentId}
|
|
1524
|
+
contentType={contentType}
|
|
1525
|
+
pageTitle={pageTitle}
|
|
1526
|
+
themeName="default"
|
|
1527
|
+
themeRegistry={themeRegistry}
|
|
1528
|
+
moduleRegistry={moduleRegistry}
|
|
1529
|
+
moduleComponents={editorComponents}
|
|
1530
|
+
onSave={handleSave}
|
|
1531
|
+
onPublish={handlePublish}
|
|
1532
|
+
/>
|
|
1533
|
+
</div>
|
|
1534
|
+
)
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Browse mode: slim toolbar + editable-region outlines over the live page.
|
|
1368
1538
|
return (
|
|
1369
1539
|
<>
|
|
1370
1540
|
<KywiEditToolbar
|
|
@@ -1372,7 +1542,7 @@ export function KywiFrontEdit({
|
|
|
1372
1542
|
canEdit={edit.canEdit}
|
|
1373
1543
|
canPublish={edit.canPublish}
|
|
1374
1544
|
onToggleEdit={edit.toggleEdit}
|
|
1375
|
-
onPublish={
|
|
1545
|
+
onPublish={handleToolbarPublish}
|
|
1376
1546
|
pageTitle={pageTitle}
|
|
1377
1547
|
pageStatus={pageStatus}
|
|
1378
1548
|
adminHref={adminHref}
|
|
@@ -1441,7 +1611,7 @@ export function PersonalizationRuntime({ audiences, serverSignals, selfIdWidget
|
|
|
1441
1611
|
currentPath,
|
|
1442
1612
|
serverSignals,
|
|
1443
1613
|
...(selfIdWidget
|
|
1444
|
-
? { selfIdWidget: { ...selfIdWidget, audiences
|
|
1614
|
+
? { selfIdWidget: { ...selfIdWidget, audiences, currentPath } }
|
|
1445
1615
|
: {}),
|
|
1446
1616
|
})
|
|
1447
1617
|
.catch(() => {
|
|
@@ -1800,7 +1970,16 @@ their Body rich text. Reach for the Layout tab when a page needs sections,
|
|
|
1800
1970
|
columns, or modules; use the Body for simple prose.
|
|
1801
1971
|
|
|
1802
1972
|
**Front-of-site editor.** Signed in as an admin, append \`?kywi-edit=1\` to any
|
|
1803
|
-
public page to get
|
|
1973
|
+
public page to edit it in place. You get the full **Layout editor** (the same
|
|
1974
|
+
drag-and-drop canvas, module palette and props panel as the admin's Layout tab),
|
|
1975
|
+
mounted right over the live page — add sections and modules, then **Save** (PUTs
|
|
1976
|
+
the layout) or **Publish** (saves + publishes). Exit the editor for the slim
|
|
1977
|
+
browse toolbar with the editable-region outlines. It all lives in
|
|
1978
|
+
\`app/(site)/kywi-front-edit.tsx\`; the editor bundle (and the admin stylesheet) is
|
|
1979
|
+
lazy-loaded, so pages your visitors see never carry its weight. Note: custom
|
|
1980
|
+
\`defineModule\` types still RENDER on the canvas, but they don't appear in the
|
|
1981
|
+
front-of-site editor's insert palette (it uses the built-in module set) — add
|
|
1982
|
+
them from the admin's Layout tab instead.
|
|
1804
1983
|
|
|
1805
1984
|
## Personalization, A/B testing & self-ID
|
|
1806
1985
|
|
|
@@ -1919,7 +2098,7 @@ lib/config.ts single import path for kywi.config.ts
|
|
|
1919
2098
|
app/api/v1/[...kywi]/route.ts the versioned API (delegates to core)
|
|
1920
2099
|
app/admin/[[...admin]]/page.tsx mounts the FULL core admin (all surfaces) at /admin
|
|
1921
2100
|
lib/modules.tsx custom (defineModule) module renderers (admin + public)
|
|
1922
|
-
app/{llms,robots,sitemap,…} root AX routes (llms.txt, robots.txt, sitemap.xml, …)${a.mode === 'coupled' ? '\napp/(site)/layout.tsx public shell: theme tokens + your header/footer\napp/(site)/site.css your site chrome styles (edit freely)\napp/(site)/[[...slug]]/page.tsx renders published pages (layout + SEO + JSON-LD + i18n)\nlib/site.ts public-render helpers: path/locale resolution, feeds, personalization\ncomponents/personalization-runtime.tsx optional client runtime (self-ID widget, live re-eval)\napp/(site)/kywi-front-edit.tsx front-of-site
|
|
2101
|
+
app/{llms,robots,sitemap,…} root AX routes (llms.txt, robots.txt, sitemap.xml, …)${a.mode === 'coupled' ? '\napp/(site)/layout.tsx public shell: theme tokens + your header/footer\napp/(site)/site.css your site chrome styles (edit freely)\napp/(site)/[[...slug]]/page.tsx renders published pages (layout + SEO + JSON-LD + i18n)\nlib/site.ts public-render helpers: path/locale resolution, feeds, personalization\ncomponents/personalization-runtime.tsx optional client runtime (self-ID widget, live re-eval)\napp/(site)/kywi-front-edit.tsx front-of-site editor: browse toolbar + in-place Layout editor (?kywi-edit=1, lazy-loaded)' : '\napp/page.tsx returns 404 (no public rendering in this mode)'}
|
|
1923
2102
|
\`\`\`
|
|
1924
2103
|
`
|
|
1925
2104
|
}
|
package/package.json
CHANGED