@astrale-os/cli 1.0.0-beta.22 → 1.0.0-beta.24
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/astrale.js +2759 -625
- package/dist/public/connect-core.js +4 -4
- package/dist/types/admin/contract.d.ts +2 -2
- package/package.json +5 -4
- package/src/admin/contract.ts +3 -2
- package/src/commands/__tests__/read-commands.test.ts +56 -0
- package/src/commands/__tests__/view.test.ts +1 -2
- package/src/commands/introspect.ts +8 -9
- package/src/ui/.spec/architecture.md +8 -7
- package/src/ui/.spec/laws.ts +7 -0
- package/src/ui/__tests__/ui.test.ts +357 -5
- package/src/ui/lock.ts +1 -1
- package/src/ui/operations.ts +89 -19
- package/src/ui/project.ts +114 -0
- package/src/ui/release.ts +9 -2
- package/studio/client/dist/assets/{index-7YHdeCxp.js → index-OeJK1TjG.js} +2 -2
- package/studio/client/dist/assets/{schema-studio-CABpTfvH.js → schema-studio-DRdfu_cQ.js} +2 -2
- package/studio/client/dist/index.html +1 -1
- package/studio/package.json +3 -3
- package/studio/server/cli-consumers.test.ts +0 -1
- package/studio/server/introspect/anatomy/views.ts +1 -2
- package/studio/server/introspect/anatomy-extras.test.ts +3 -14
- package/studio/server/introspect/canonical-schema.test.ts +1 -1
- package/studio/server/introspect/canonical-schema.ts +0 -4
- package/studio/server/introspect/diff.test.ts +10 -2
- package/studio/server/introspect/diff.ts +1 -9
- package/studio/server/introspect/runtime.test.ts +1 -1
- package/studio/server/introspect/schema-ir-json.test.ts +12 -1
- package/studio/server/introspect/schema-ir-json.ts +1 -1
- package/studio/server/views/target.test.ts +1 -2
- package/studio/shared/contracts/schema.ts +1 -3
- package/studio/tsconfig.json +2 -1
- package/viewer/dist/main.js +13 -13
|
@@ -28,6 +28,30 @@ const registry: UiRegistry = {
|
|
|
28
28
|
},
|
|
29
29
|
],
|
|
30
30
|
}
|
|
31
|
+
const componentRegistry: UiRegistry = {
|
|
32
|
+
name: 'astrale-ui',
|
|
33
|
+
items: [
|
|
34
|
+
{
|
|
35
|
+
name: 'component-sidebar',
|
|
36
|
+
type: 'registry:component',
|
|
37
|
+
title: 'Component · sidebar',
|
|
38
|
+
dependencies: ['@astrale-os/ui@^0.3.0-beta.0', 'lucide-react@1.32.0'],
|
|
39
|
+
files: [
|
|
40
|
+
{
|
|
41
|
+
path: 'sidebar.tsx',
|
|
42
|
+
type: 'registry:component',
|
|
43
|
+
target: 'components/astrale/component/sidebar/sidebar.tsx',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
path: 'use-mobile.ts',
|
|
47
|
+
type: 'registry:hook',
|
|
48
|
+
target: 'components/astrale/component/sidebar/use-mobile.ts',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
meta: { canonicalAddress: 'component/sidebar', ownership: 'consumer-source' },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}
|
|
31
55
|
const themeCss = `/* Generated from observatory.astrale-theme.json. Consumer-owned after installation. */
|
|
32
56
|
:root,
|
|
33
57
|
[data-ui-theme='observatory'] {
|
|
@@ -257,15 +281,17 @@ function lock(): UiLock {
|
|
|
257
281
|
}
|
|
258
282
|
|
|
259
283
|
describe('UI release and runner contracts', () => {
|
|
284
|
+
/** @evidence TEST-CLI-UI-BETA-DEFAULT */
|
|
260
285
|
test('resolves the default release from the public beta channel', async () => {
|
|
261
286
|
const seen: string[] = []
|
|
287
|
+
const fallback = mockFetch()
|
|
262
288
|
const fetcher = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
263
289
|
const url = String(input)
|
|
290
|
+
seen.push(url)
|
|
264
291
|
if (url.endsWith('/@astrale-os/ui/beta')) {
|
|
265
|
-
seen.push(url)
|
|
266
292
|
return Response.json({ version: '0.3.0-beta.1' })
|
|
267
293
|
}
|
|
268
|
-
return
|
|
294
|
+
return fallback(input, init)
|
|
269
295
|
}) as typeof fetch
|
|
270
296
|
|
|
271
297
|
const release = await resolveUiRelease(undefined, fetcher)
|
|
@@ -322,6 +348,36 @@ describe('UI release and runner contracts', () => {
|
|
|
322
348
|
])
|
|
323
349
|
})
|
|
324
350
|
|
|
351
|
+
test('admits an exact consumer-owned component with item-local dependencies and files', async () => {
|
|
352
|
+
const release = await resolveUiRelease('0.3.0-beta.0', mockFetch([], componentRegistry))
|
|
353
|
+
expect(release.registry.items).toEqual([
|
|
354
|
+
expect.objectContaining({
|
|
355
|
+
name: 'component-sidebar',
|
|
356
|
+
type: 'registry:component',
|
|
357
|
+
dependencies: ['@astrale-os/ui@^0.3.0-beta.0', 'lucide-react@1.32.0'],
|
|
358
|
+
meta: expect.objectContaining({ canonicalAddress: 'component/sidebar' }),
|
|
359
|
+
files: [
|
|
360
|
+
expect.objectContaining({
|
|
361
|
+
type: 'registry:component',
|
|
362
|
+
target: 'components/astrale/component/sidebar/sidebar.tsx',
|
|
363
|
+
}),
|
|
364
|
+
expect.objectContaining({
|
|
365
|
+
type: 'registry:hook',
|
|
366
|
+
target: 'components/astrale/component/sidebar/use-mobile.ts',
|
|
367
|
+
}),
|
|
368
|
+
],
|
|
369
|
+
}),
|
|
370
|
+
])
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
test('rejects a component item that targets another registry ownership category', async () => {
|
|
374
|
+
const invalid = structuredClone(componentRegistry)
|
|
375
|
+
invalid.items[0]!.files[0]!.target = 'components/astrale/pattern/sidebar/sidebar.tsx'
|
|
376
|
+
await expect(resolveUiRelease('0.3.0-beta.0', mockFetch([], invalid))).rejects.toMatchObject({
|
|
377
|
+
code: 'UI_REGISTRY_UNAVAILABLE',
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
|
|
325
381
|
test('lists release themes through the public type filter', async () => {
|
|
326
382
|
const themes = await listUi(
|
|
327
383
|
undefined,
|
|
@@ -795,8 +851,60 @@ describe('UI initialization transaction', () => {
|
|
|
795
851
|
)
|
|
796
852
|
})
|
|
797
853
|
|
|
854
|
+
test('preserves an existing development-only UI dependency during initialization', async () => {
|
|
855
|
+
const root = await fixture()
|
|
856
|
+
const manifestPath = path.join(root, 'package.json')
|
|
857
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
858
|
+
manifest.devDependencies = { '@astrale-os/ui': '0.3.0-beta.0' }
|
|
859
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
860
|
+
|
|
861
|
+
await initUi({ path: root, version: '0.3.0-beta.0', install: false }, { fetcher: mockFetch() })
|
|
862
|
+
|
|
863
|
+
const written = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
864
|
+
expect(written.dependencies['@astrale-os/ui']).toBeUndefined()
|
|
865
|
+
expect(written.devDependencies['@astrale-os/ui']).toBe('0.3.0-beta.0')
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
test('doctor accepts one exact development dependency and rejects drift or duplication', async () => {
|
|
869
|
+
const root = await lockedFixture()
|
|
870
|
+
const manifestPath = path.join(root, 'package.json')
|
|
871
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
872
|
+
delete manifest.dependencies['@astrale-os/ui']
|
|
873
|
+
manifest.devDependencies = { '@astrale-os/ui': '0.3.0-beta.0' }
|
|
874
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
875
|
+
await writeFile(
|
|
876
|
+
path.join(root, 'components.json'),
|
|
877
|
+
JSON.stringify({ style: 'base-nova', tailwind: { css: 'src/index.css' } }),
|
|
878
|
+
)
|
|
879
|
+
await writeFile(
|
|
880
|
+
path.join(root, 'src/index.css'),
|
|
881
|
+
"@import '@astrale-os/ui/theme.css';\n@import '@astrale-os/ui/presets/astrale.css';\n",
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
expect((await doctorUi(root)).healthy).toBe(true)
|
|
885
|
+
|
|
886
|
+
manifest.devDependencies['@astrale-os/ui'] = '0.3.0-beta.1'
|
|
887
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
888
|
+
expect((await doctorUi(root)).checks.find(({ check }) => check === 'package')).toMatchObject({
|
|
889
|
+
ok: false,
|
|
890
|
+
detail: 'devDependencies:0.3.0-beta.1',
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
manifest.devDependencies['@astrale-os/ui'] = '0.3.0-beta.0'
|
|
894
|
+
manifest.dependencies['@astrale-os/ui'] = '0.3.0-beta.0'
|
|
895
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
896
|
+
expect((await doctorUi(root)).checks.find(({ check }) => check === 'package')).toMatchObject({
|
|
897
|
+
ok: false,
|
|
898
|
+
detail: 'declared in dependencies and devDependencies',
|
|
899
|
+
})
|
|
900
|
+
})
|
|
901
|
+
|
|
798
902
|
test('repeated exact init performs no release fetch while requested drift rejects', async () => {
|
|
799
903
|
const root = await fixture()
|
|
904
|
+
const manifestPath = path.join(root, 'package.json')
|
|
905
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
906
|
+
manifest.dependencies['@astrale-os/ui'] = '0.3.0-beta.0'
|
|
907
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
800
908
|
await writeFile(path.join(root, 'astrale-ui.lock.json'), JSON.stringify(lock()))
|
|
801
909
|
await writeFile(
|
|
802
910
|
path.join(root, 'components.json'),
|
|
@@ -821,6 +929,11 @@ describe('UI initialization transaction', () => {
|
|
|
821
929
|
await expect(initUi({ path: root, preset: 'compact' })).rejects.toMatchObject({
|
|
822
930
|
code: 'UI_ITEM_CONFLICT',
|
|
823
931
|
})
|
|
932
|
+
manifest.dependencies['@astrale-os/ui'] = '0.3.0-beta.1'
|
|
933
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
934
|
+
await expect(initUi({ path: root, preset: 'astrale' })).rejects.toMatchObject({
|
|
935
|
+
code: 'UI_ITEM_CONFLICT',
|
|
936
|
+
})
|
|
824
937
|
})
|
|
825
938
|
})
|
|
826
939
|
|
|
@@ -1096,6 +1209,201 @@ describe('UI source operations', () => {
|
|
|
1096
1209
|
).rejects.toBeInstanceOf(UiError)
|
|
1097
1210
|
})
|
|
1098
1211
|
|
|
1212
|
+
test('records multi-file component targets resolved through the consumer components alias', async () => {
|
|
1213
|
+
const root = await lockedFixture()
|
|
1214
|
+
await writeFile(
|
|
1215
|
+
path.join(root, 'components.json'),
|
|
1216
|
+
JSON.stringify({
|
|
1217
|
+
style: 'base-nova',
|
|
1218
|
+
tailwind: { css: 'src/index.css' },
|
|
1219
|
+
aliases: { components: '@/components' },
|
|
1220
|
+
}),
|
|
1221
|
+
)
|
|
1222
|
+
await writeFile(
|
|
1223
|
+
path.join(root, 'tsconfig.json'),
|
|
1224
|
+
JSON.stringify({
|
|
1225
|
+
compilerOptions: {
|
|
1226
|
+
paths: { '@/*': ['./src/*'], '@/components/*': ['./app/ui/*'] },
|
|
1227
|
+
},
|
|
1228
|
+
}),
|
|
1229
|
+
)
|
|
1230
|
+
await writeFile(
|
|
1231
|
+
path.join(root, 'src/index.css'),
|
|
1232
|
+
"@import '@astrale-os/ui/theme.css';\n@import '@astrale-os/ui/presets/astrale.css';\n",
|
|
1233
|
+
)
|
|
1234
|
+
const sidebar = path.join(root, 'src/components/astrale/component/sidebar/sidebar.tsx')
|
|
1235
|
+
const hook = path.join(root, 'src/components/astrale/component/sidebar/use-mobile.ts')
|
|
1236
|
+
|
|
1237
|
+
const planned = await addUi(
|
|
1238
|
+
['component/sidebar'],
|
|
1239
|
+
{ project: root, dryRun: true, yes: true },
|
|
1240
|
+
{
|
|
1241
|
+
fetcher: mockFetch([], componentRegistry),
|
|
1242
|
+
runner: async () => ({ code: 0, stdout: 'planned', stderr: '' }),
|
|
1243
|
+
},
|
|
1244
|
+
)
|
|
1245
|
+
expect(planned.sources).toEqual([
|
|
1246
|
+
expect.objectContaining({
|
|
1247
|
+
files: [
|
|
1248
|
+
'src/components/astrale/component/sidebar/sidebar.tsx',
|
|
1249
|
+
'src/components/astrale/component/sidebar/use-mobile.ts',
|
|
1250
|
+
],
|
|
1251
|
+
}),
|
|
1252
|
+
])
|
|
1253
|
+
|
|
1254
|
+
await addUi(
|
|
1255
|
+
['component/sidebar'],
|
|
1256
|
+
{ project: root, yes: true },
|
|
1257
|
+
{
|
|
1258
|
+
fetcher: mockFetch([], componentRegistry),
|
|
1259
|
+
runner: async () => {
|
|
1260
|
+
await mkdir(path.dirname(sidebar), { recursive: true })
|
|
1261
|
+
await writeFile(sidebar, 'export const Sidebar = true\n')
|
|
1262
|
+
await writeFile(hook, 'export const useMobile = true\n')
|
|
1263
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
1264
|
+
},
|
|
1265
|
+
},
|
|
1266
|
+
)
|
|
1267
|
+
|
|
1268
|
+
const written = JSON.parse(await readFile(path.join(root, 'astrale-ui.lock.json'), 'utf8'))
|
|
1269
|
+
expect(written.items['component/sidebar'].files).toEqual({
|
|
1270
|
+
'src/components/astrale/component/sidebar/sidebar.tsx': digest(
|
|
1271
|
+
'export const Sidebar = true\n',
|
|
1272
|
+
),
|
|
1273
|
+
'src/components/astrale/component/sidebar/use-mobile.ts': digest(
|
|
1274
|
+
'export const useMobile = true\n',
|
|
1275
|
+
),
|
|
1276
|
+
})
|
|
1277
|
+
expect((await doctorUi(root)).healthy).toBe(true)
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
test('resolves a components alias through package imports before tsconfig paths', async () => {
|
|
1281
|
+
const root = await lockedFixture()
|
|
1282
|
+
const manifestPath = path.join(root, 'package.json')
|
|
1283
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
1284
|
+
manifest.imports = { '#app/*': './src/app/*' }
|
|
1285
|
+
await writeFile(manifestPath, JSON.stringify(manifest))
|
|
1286
|
+
await writeFile(
|
|
1287
|
+
path.join(root, 'components.json'),
|
|
1288
|
+
JSON.stringify({
|
|
1289
|
+
style: 'base-nova',
|
|
1290
|
+
tailwind: { css: 'src/index.css' },
|
|
1291
|
+
aliases: { components: '#app/components' },
|
|
1292
|
+
}),
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
const planned = await addUi(
|
|
1296
|
+
['pattern/chart/line/basic'],
|
|
1297
|
+
{ project: root, dryRun: true, yes: true },
|
|
1298
|
+
{
|
|
1299
|
+
fetcher: mockFetch(),
|
|
1300
|
+
runner: async () => ({ code: 0, stdout: 'planned', stderr: '' }),
|
|
1301
|
+
},
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
expect(planned.sources).toEqual([
|
|
1305
|
+
expect.objectContaining({
|
|
1306
|
+
files: ['src/app/components/astrale/pattern/chart/line-basic.tsx'],
|
|
1307
|
+
}),
|
|
1308
|
+
])
|
|
1309
|
+
})
|
|
1310
|
+
|
|
1311
|
+
test('rejects an unresolved package-import alias before invoking shadcn', async () => {
|
|
1312
|
+
const root = await lockedFixture()
|
|
1313
|
+
await writeFile(
|
|
1314
|
+
path.join(root, 'components.json'),
|
|
1315
|
+
JSON.stringify({
|
|
1316
|
+
style: 'base-nova',
|
|
1317
|
+
tailwind: { css: 'src/index.css' },
|
|
1318
|
+
aliases: { components: '#missing/components' },
|
|
1319
|
+
}),
|
|
1320
|
+
)
|
|
1321
|
+
let invoked = false
|
|
1322
|
+
|
|
1323
|
+
await expect(
|
|
1324
|
+
addUi(
|
|
1325
|
+
['pattern/chart/line/basic'],
|
|
1326
|
+
{ project: root, yes: true },
|
|
1327
|
+
{
|
|
1328
|
+
fetcher: mockFetch(),
|
|
1329
|
+
runner: async () => {
|
|
1330
|
+
invoked = true
|
|
1331
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
1332
|
+
},
|
|
1333
|
+
},
|
|
1334
|
+
),
|
|
1335
|
+
).rejects.toMatchObject({ code: 'UI_PROJECT_UNSUPPORTED' })
|
|
1336
|
+
expect(invoked).toBe(false)
|
|
1337
|
+
})
|
|
1338
|
+
|
|
1339
|
+
test('resolves an extended JSONC baseUrl with the pinned shadcn config loader', async () => {
|
|
1340
|
+
const root = await lockedFixture()
|
|
1341
|
+
await writeFile(
|
|
1342
|
+
path.join(root, 'components.json'),
|
|
1343
|
+
JSON.stringify({
|
|
1344
|
+
style: 'base-nova',
|
|
1345
|
+
tailwind: { css: 'src/index.css' },
|
|
1346
|
+
aliases: { components: '@/components' },
|
|
1347
|
+
}),
|
|
1348
|
+
)
|
|
1349
|
+
await mkdir(path.join(root, 'config'), { recursive: true })
|
|
1350
|
+
await writeFile(
|
|
1351
|
+
path.join(root, 'tsconfig.json'),
|
|
1352
|
+
'{\n // shadcn loads this root config.\n "extends": "./config/base.json",\n}\n',
|
|
1353
|
+
)
|
|
1354
|
+
await writeFile(
|
|
1355
|
+
path.join(root, 'config/base.json'),
|
|
1356
|
+
'{\n "compilerOptions": {\n "baseUrl": "..",\n "paths": { "@/*": ["frontend/src/*"], },\n },\n}\n',
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
const planned = await addUi(
|
|
1360
|
+
['pattern/chart/line/basic'],
|
|
1361
|
+
{ project: root, dryRun: true, yes: true },
|
|
1362
|
+
{
|
|
1363
|
+
fetcher: mockFetch(),
|
|
1364
|
+
runner: async () => ({ code: 0, stdout: 'planned', stderr: '' }),
|
|
1365
|
+
},
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
expect(planned.sources).toEqual([
|
|
1369
|
+
expect.objectContaining({
|
|
1370
|
+
files: ['frontend/src/components/astrale/pattern/chart/line-basic.tsx'],
|
|
1371
|
+
}),
|
|
1372
|
+
])
|
|
1373
|
+
})
|
|
1374
|
+
|
|
1375
|
+
test('rejects an alias mapping outside the project before invoking shadcn', async () => {
|
|
1376
|
+
const root = await lockedFixture()
|
|
1377
|
+
await writeFile(
|
|
1378
|
+
path.join(root, 'components.json'),
|
|
1379
|
+
JSON.stringify({
|
|
1380
|
+
style: 'base-nova',
|
|
1381
|
+
tailwind: { css: 'src/index.css' },
|
|
1382
|
+
aliases: { components: '@/components' },
|
|
1383
|
+
}),
|
|
1384
|
+
)
|
|
1385
|
+
await writeFile(
|
|
1386
|
+
path.join(root, 'tsconfig.json'),
|
|
1387
|
+
JSON.stringify({ compilerOptions: { paths: { '@/*': ['../outside/*'] } } }),
|
|
1388
|
+
)
|
|
1389
|
+
let invoked = false
|
|
1390
|
+
|
|
1391
|
+
await expect(
|
|
1392
|
+
addUi(
|
|
1393
|
+
['pattern/chart/line/basic'],
|
|
1394
|
+
{ project: root, yes: true },
|
|
1395
|
+
{
|
|
1396
|
+
fetcher: mockFetch(),
|
|
1397
|
+
runner: async () => {
|
|
1398
|
+
invoked = true
|
|
1399
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
1400
|
+
},
|
|
1401
|
+
},
|
|
1402
|
+
),
|
|
1403
|
+
).rejects.toMatchObject({ code: 'UI_PROJECT_UNSUPPORTED' })
|
|
1404
|
+
expect(invoked).toBe(false)
|
|
1405
|
+
})
|
|
1406
|
+
|
|
1099
1407
|
test('restores the exact locked UI dependency after shadcn applies its compatible range', async () => {
|
|
1100
1408
|
const root = await lockedFixture()
|
|
1101
1409
|
const installed = path.join(root, 'components/astrale/pattern/chart/line-basic.tsx')
|
|
@@ -1132,6 +1440,38 @@ describe('UI source operations', () => {
|
|
|
1132
1440
|
).toBe('0.3.0-beta.0')
|
|
1133
1441
|
})
|
|
1134
1442
|
|
|
1443
|
+
test('restores a development-only UI dependency without retaining shadcn duplication', async () => {
|
|
1444
|
+
const root = await lockedFixture()
|
|
1445
|
+
const manifestPath = path.join(root, 'package.json')
|
|
1446
|
+
const initial = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
1447
|
+
delete initial.dependencies['@astrale-os/ui']
|
|
1448
|
+
initial.devDependencies = { '@astrale-os/ui': '0.3.0-beta.0' }
|
|
1449
|
+
await writeFile(manifestPath, JSON.stringify(initial))
|
|
1450
|
+
const installed = path.join(root, 'components/astrale/pattern/chart/line-basic.tsx')
|
|
1451
|
+
|
|
1452
|
+
await addUi(
|
|
1453
|
+
['pattern/chart/line/basic'],
|
|
1454
|
+
{ project: root, yes: true },
|
|
1455
|
+
{
|
|
1456
|
+
fetcher: mockFetch(),
|
|
1457
|
+
runner: async (_file, args) => {
|
|
1458
|
+
if (args[0] === 'dlx') {
|
|
1459
|
+
await mkdir(path.dirname(installed), { recursive: true })
|
|
1460
|
+
await writeFile(installed, 'export const Chart = true\n')
|
|
1461
|
+
const changed = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
1462
|
+
changed.dependencies['@astrale-os/ui'] = '^0.3.0-beta.0'
|
|
1463
|
+
await writeFile(manifestPath, JSON.stringify(changed))
|
|
1464
|
+
}
|
|
1465
|
+
return { code: 0, stdout: '', stderr: '' }
|
|
1466
|
+
},
|
|
1467
|
+
},
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
const written = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
1471
|
+
expect(written.dependencies['@astrale-os/ui']).toBeUndefined()
|
|
1472
|
+
expect(written.devDependencies['@astrale-os/ui']).toBe('0.3.0-beta.0')
|
|
1473
|
+
})
|
|
1474
|
+
|
|
1135
1475
|
test('rolls back item and package state when restoring the locked dependency fails', async () => {
|
|
1136
1476
|
const root = await lockedFixture()
|
|
1137
1477
|
const manifestPath = path.join(root, 'package.json')
|
|
@@ -1190,10 +1530,22 @@ describe('UI source operations', () => {
|
|
|
1190
1530
|
expect(invoked).toBe(false)
|
|
1191
1531
|
})
|
|
1192
1532
|
|
|
1193
|
-
test('restores
|
|
1533
|
+
test('restores alias-resolved files and package state after a partial shadcn failure', async () => {
|
|
1194
1534
|
const root = await lockedFixture()
|
|
1195
|
-
|
|
1196
|
-
|
|
1535
|
+
await writeFile(
|
|
1536
|
+
path.join(root, 'components.json'),
|
|
1537
|
+
JSON.stringify({
|
|
1538
|
+
style: 'base-nova',
|
|
1539
|
+
tailwind: { css: 'src/index.css' },
|
|
1540
|
+
aliases: { components: '@/components' },
|
|
1541
|
+
}),
|
|
1542
|
+
)
|
|
1543
|
+
await writeFile(
|
|
1544
|
+
path.join(root, 'tsconfig.json'),
|
|
1545
|
+
JSON.stringify({ compilerOptions: { paths: { '@/*': ['./src/*'] } } }),
|
|
1546
|
+
)
|
|
1547
|
+
const first = path.join(root, 'src/components/astrale/pattern/chart/line-basic.tsx')
|
|
1548
|
+
const second = path.join(root, 'src/components/astrale/pattern/chart/summary.tsx')
|
|
1197
1549
|
await mkdir(path.dirname(first), { recursive: true })
|
|
1198
1550
|
await writeFile(first, 'consumer original\n')
|
|
1199
1551
|
const twoFileRegistry: UiRegistry = {
|
package/src/ui/lock.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function parseUiLock(value: unknown): UiLock {
|
|
|
32
32
|
}
|
|
33
33
|
for (const [address, item] of Object.entries(lock.items)) {
|
|
34
34
|
if (
|
|
35
|
-
!/^(?:(?:pattern|block)\/[a-z0-9-]+\/[a-z0-9-/]+|theme\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u.test(
|
|
35
|
+
!/^(?:component\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*|(?:pattern|block)\/[a-z0-9-]+\/[a-z0-9-/]+|theme\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/u.test(
|
|
36
36
|
address,
|
|
37
37
|
) ||
|
|
38
38
|
!isRecord(item) ||
|
package/src/ui/operations.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
assertSupportedUiProject,
|
|
18
18
|
discoverUiProject,
|
|
19
19
|
projectRelative,
|
|
20
|
+
resolveUiRegistryTarget,
|
|
20
21
|
type UiProject,
|
|
21
22
|
} from './project'
|
|
22
23
|
import {
|
|
@@ -134,8 +135,45 @@ function activateTheme(current: string, statement: string): string {
|
|
|
134
135
|
.replace(/^\n/u, '')
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
type ManifestDependencySection = 'dependencies' | 'devDependencies'
|
|
139
|
+
|
|
140
|
+
function manifestDependencies(
|
|
141
|
+
manifest: Record<string, unknown>,
|
|
142
|
+
section: ManifestDependencySection = 'dependencies',
|
|
143
|
+
): Record<string, string> {
|
|
144
|
+
return { ...(manifest[section] as Record<string, string> | undefined) }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function uiDependencySection(
|
|
148
|
+
manifest: Record<string, unknown>,
|
|
149
|
+
fallback: ManifestDependencySection = 'dependencies',
|
|
150
|
+
): ManifestDependencySection {
|
|
151
|
+
const runtime = Object.hasOwn(manifestDependencies(manifest), UI_PACKAGE)
|
|
152
|
+
const development = Object.hasOwn(manifestDependencies(manifest, 'devDependencies'), UI_PACKAGE)
|
|
153
|
+
if (runtime && development) {
|
|
154
|
+
throw new UiError(
|
|
155
|
+
'UI_PROJECT_UNSUPPORTED',
|
|
156
|
+
UI_PACKAGE + ' must be declared in exactly one dependency section.',
|
|
157
|
+
'Remove it from either dependencies or devDependencies, then retry.',
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
if (development) return 'devDependencies'
|
|
161
|
+
if (runtime) return 'dependencies'
|
|
162
|
+
return fallback
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function setUiDependency(
|
|
166
|
+
manifest: Record<string, unknown>,
|
|
167
|
+
section: ManifestDependencySection,
|
|
168
|
+
version: string,
|
|
169
|
+
): void {
|
|
170
|
+
const other = section === 'dependencies' ? 'devDependencies' : 'dependencies'
|
|
171
|
+
manifest[section] = { ...manifestDependencies(manifest, section), [UI_PACKAGE]: version }
|
|
172
|
+
const otherDependencies = manifestDependencies(manifest, other)
|
|
173
|
+
if (Object.hasOwn(otherDependencies, UI_PACKAGE)) {
|
|
174
|
+
delete otherDependencies[UI_PACKAGE]
|
|
175
|
+
manifest[other] = otherDependencies
|
|
176
|
+
}
|
|
139
177
|
}
|
|
140
178
|
|
|
141
179
|
function domainRegistryPackagePath(project: UiProject): string {
|
|
@@ -294,6 +332,10 @@ export async function initUi(
|
|
|
294
332
|
}
|
|
295
333
|
if ((await exists(project.uiLockPath)) && !options.force) {
|
|
296
334
|
const lock = await readUiLock(project.uiLockPath)
|
|
335
|
+
const dependencySection = uiDependencySection(project.packageJson)
|
|
336
|
+
const dependencyVersion = manifestDependencies(project.packageJson, dependencySection)[
|
|
337
|
+
UI_PACKAGE
|
|
338
|
+
]
|
|
297
339
|
const css = (await readOptional(project.cssPath)) ?? ''
|
|
298
340
|
const components = await readOptional(project.componentsPath)
|
|
299
341
|
.then((value) => (value ? (JSON.parse(value) as Record<string, unknown>) : undefined))
|
|
@@ -301,6 +343,7 @@ export async function initUi(
|
|
|
301
343
|
const requestedVersion = options.version?.replace(/^v/u, '')
|
|
302
344
|
const desired =
|
|
303
345
|
(!requestedVersion || requestedVersion === lock.package.version) &&
|
|
346
|
+
dependencyVersion === lock.package.version &&
|
|
304
347
|
(!options.preset || options.preset === lock.preset) &&
|
|
305
348
|
css.includes(UI_PACKAGE + '/theme.css') &&
|
|
306
349
|
css.includes(UI_PACKAGE + '/presets/' + lock.preset + '.css') &&
|
|
@@ -362,10 +405,7 @@ export async function initUi(
|
|
|
362
405
|
|
|
363
406
|
try {
|
|
364
407
|
const manifest = { ...project.packageJson }
|
|
365
|
-
manifest
|
|
366
|
-
...manifestDependencies(manifest),
|
|
367
|
-
[UI_PACKAGE]: release.version,
|
|
368
|
-
}
|
|
408
|
+
setUiDependency(manifest, uiDependencySection(manifest), release.version)
|
|
369
409
|
await writeDomainRegistryWorkspace(project, manifest)
|
|
370
410
|
await writeJson(project.packageJsonPath, manifest)
|
|
371
411
|
|
|
@@ -461,14 +501,17 @@ export async function initUi(
|
|
|
461
501
|
async function pinUiDependency(
|
|
462
502
|
project: UiProject,
|
|
463
503
|
version: string,
|
|
504
|
+
section: ManifestDependencySection,
|
|
464
505
|
runner: UiRunner,
|
|
465
506
|
): Promise<void> {
|
|
466
507
|
const manifest = JSON.parse(await readFile(project.packageJsonPath, 'utf8')) as Record<
|
|
467
508
|
string,
|
|
468
509
|
unknown
|
|
469
510
|
>
|
|
470
|
-
|
|
471
|
-
|
|
511
|
+
const current = manifestDependencies(manifest, section)[UI_PACKAGE]
|
|
512
|
+
const other = section === 'dependencies' ? 'devDependencies' : 'dependencies'
|
|
513
|
+
if (current === version && manifestDependencies(manifest, other)[UI_PACKAGE] === undefined) return
|
|
514
|
+
setUiDependency(manifest, section, version)
|
|
472
515
|
await writeJson(project.packageJsonPath, manifest)
|
|
473
516
|
const result = await runner(project.manager, ['install'], project.root)
|
|
474
517
|
if (result.code !== 0) {
|
|
@@ -663,6 +706,7 @@ export async function addUi(
|
|
|
663
706
|
return addLocalTheme(localThemes[0]!, project, options)
|
|
664
707
|
}
|
|
665
708
|
const { lock, release } = await lockedRelease(project, dependencies.fetcher)
|
|
709
|
+
const dependencySection = uiDependencySection(project.packageJson)
|
|
666
710
|
const items = addresses.map((address) => findItem(release, address))
|
|
667
711
|
const themes = items.filter((item) => item.type === 'registry:theme')
|
|
668
712
|
if (themes.length > 1) {
|
|
@@ -681,13 +725,19 @@ export async function addUi(
|
|
|
681
725
|
return addReleasedTheme(themes[0]!, itemDocuments[0]!, project, options)
|
|
682
726
|
}
|
|
683
727
|
await rejectLocalChanges(project, lock, items, options.overwrite === true)
|
|
684
|
-
const
|
|
728
|
+
const declaredTargets = items.flatMap((item) =>
|
|
729
|
+
item.files.flatMap((file) => (file.target ? [file.target] : [])),
|
|
730
|
+
)
|
|
731
|
+
const resolvedTargets = new Map(
|
|
685
732
|
await Promise.all(
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
.filter((file) => file.target)
|
|
689
|
-
.map((file) => assertSafePlannedTarget(project, file.target!)),
|
|
733
|
+
declaredTargets.map(
|
|
734
|
+
async (target) => [target, await resolveUiRegistryTarget(project, target)] as const,
|
|
690
735
|
),
|
|
736
|
+
),
|
|
737
|
+
)
|
|
738
|
+
const targets = (
|
|
739
|
+
await Promise.all(
|
|
740
|
+
[...resolvedTargets.values()].map((target) => assertSafePlannedTarget(project, target)),
|
|
691
741
|
)
|
|
692
742
|
).filter((target, index, all) => all.indexOf(target) === index)
|
|
693
743
|
const invocation = shadcnInvocation(project.manager, release.compatibility.shadcn, [
|
|
@@ -731,7 +781,12 @@ export async function addUi(
|
|
|
731
781
|
throw new UiError('UI_TOOL_FAILED', 'The pinned shadcn operation omitted an item file.')
|
|
732
782
|
}
|
|
733
783
|
}
|
|
734
|
-
await pinUiDependency(
|
|
784
|
+
await pinUiDependency(
|
|
785
|
+
project,
|
|
786
|
+
lock.package.version,
|
|
787
|
+
dependencySection,
|
|
788
|
+
dependencies.runner ?? defaultUiRunner,
|
|
789
|
+
)
|
|
735
790
|
const theme = themes[0]
|
|
736
791
|
const themeTarget = theme?.files[0]?.target
|
|
737
792
|
if (themeTarget) {
|
|
@@ -754,7 +809,9 @@ export async function addUi(
|
|
|
754
809
|
sources: itemDocuments.map((item) => ({
|
|
755
810
|
address: item.meta.canonicalAddress,
|
|
756
811
|
dependencies: item.dependencies ?? [],
|
|
757
|
-
files: item.files
|
|
812
|
+
files: item.files
|
|
813
|
+
.map((file) => (file.target ? resolvedTargets.get(file.target) : undefined))
|
|
814
|
+
.filter(Boolean),
|
|
758
815
|
})),
|
|
759
816
|
command: [invocation.file, ...invocation.args],
|
|
760
817
|
output: result.stdout,
|
|
@@ -776,7 +833,7 @@ export async function addUi(
|
|
|
776
833
|
const files: Record<string, string> = {}
|
|
777
834
|
for (const file of item.files) {
|
|
778
835
|
if (!file.target) continue
|
|
779
|
-
const target = await safeTarget(project, file.target)
|
|
836
|
+
const target = await safeTarget(project, resolvedTargets.get(file.target)!)
|
|
780
837
|
files[projectRelative(project, target)] = digest(await readFile(target))
|
|
781
838
|
}
|
|
782
839
|
lock.items[item.meta.canonicalAddress] = {
|
|
@@ -821,11 +878,24 @@ export async function doctorUi(
|
|
|
821
878
|
detail: error instanceof Error ? error.message : String(error),
|
|
822
879
|
})
|
|
823
880
|
}
|
|
824
|
-
const
|
|
881
|
+
const runtimeVersion = manifestDependencies(project.packageJson)[UI_PACKAGE]
|
|
882
|
+
const developmentVersion = manifestDependencies(project.packageJson, 'devDependencies')[
|
|
883
|
+
UI_PACKAGE
|
|
884
|
+
]
|
|
885
|
+
const declarations = [runtimeVersion, developmentVersion].filter(
|
|
886
|
+
(version): version is string => typeof version === 'string',
|
|
887
|
+
)
|
|
888
|
+
const packageVersion = declarations.length === 1 ? declarations[0] : undefined
|
|
825
889
|
checks.push({
|
|
826
890
|
check: 'package',
|
|
827
|
-
ok:
|
|
828
|
-
detail:
|
|
891
|
+
ok: declarations.length === 1 && (!lock || packageVersion === lock.package.version),
|
|
892
|
+
detail:
|
|
893
|
+
declarations.length > 1
|
|
894
|
+
? 'declared in dependencies and devDependencies'
|
|
895
|
+
: packageVersion === undefined
|
|
896
|
+
? 'missing'
|
|
897
|
+
: (runtimeVersion === packageVersion ? 'dependencies:' : 'devDependencies:') +
|
|
898
|
+
packageVersion,
|
|
829
899
|
})
|
|
830
900
|
const css = (await readOptional(project.cssPath)) ?? ''
|
|
831
901
|
checks.push({ check: 'theme', ok: css.includes(UI_PACKAGE + '/theme.css') })
|