@astrale-os/cli 1.0.0-beta.23 → 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 +2110 -6
- package/package.json +2 -1
- package/src/ui/__tests__/ui.test.ts +210 -3
- package/src/ui/lock.ts +1 -1
- package/src/ui/operations.ts +16 -7
- package/src/ui/project.ts +114 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.24",
|
|
4
4
|
"description": "Astrale CLI — connect to existing Astrale kernels",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astrale",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"jose": "^6.1.3",
|
|
58
58
|
"ora": "^9.0.0",
|
|
59
59
|
"ts-morph": "^25.0.1",
|
|
60
|
+
"tsconfig-paths": "4.2.0",
|
|
60
61
|
"yaml": "^2.8.2",
|
|
61
62
|
"zod": "^4.4.3"
|
|
62
63
|
},
|
|
@@ -1209,6 +1209,201 @@ describe('UI source operations', () => {
|
|
|
1209
1209
|
).rejects.toBeInstanceOf(UiError)
|
|
1210
1210
|
})
|
|
1211
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
|
+
|
|
1212
1407
|
test('restores the exact locked UI dependency after shadcn applies its compatible range', async () => {
|
|
1213
1408
|
const root = await lockedFixture()
|
|
1214
1409
|
const installed = path.join(root, 'components/astrale/pattern/chart/line-basic.tsx')
|
|
@@ -1335,10 +1530,22 @@ describe('UI source operations', () => {
|
|
|
1335
1530
|
expect(invoked).toBe(false)
|
|
1336
1531
|
})
|
|
1337
1532
|
|
|
1338
|
-
test('restores
|
|
1533
|
+
test('restores alias-resolved files and package state after a partial shadcn failure', async () => {
|
|
1339
1534
|
const root = await lockedFixture()
|
|
1340
|
-
|
|
1341
|
-
|
|
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')
|
|
1342
1549
|
await mkdir(path.dirname(first), { recursive: true })
|
|
1343
1550
|
await writeFile(first, 'consumer original\n')
|
|
1344
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 {
|
|
@@ -724,13 +725,19 @@ export async function addUi(
|
|
|
724
725
|
return addReleasedTheme(themes[0]!, itemDocuments[0]!, project, options)
|
|
725
726
|
}
|
|
726
727
|
await rejectLocalChanges(project, lock, items, options.overwrite === true)
|
|
727
|
-
const
|
|
728
|
+
const declaredTargets = items.flatMap((item) =>
|
|
729
|
+
item.files.flatMap((file) => (file.target ? [file.target] : [])),
|
|
730
|
+
)
|
|
731
|
+
const resolvedTargets = new Map(
|
|
728
732
|
await Promise.all(
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
.filter((file) => file.target)
|
|
732
|
-
.map((file) => assertSafePlannedTarget(project, file.target!)),
|
|
733
|
+
declaredTargets.map(
|
|
734
|
+
async (target) => [target, await resolveUiRegistryTarget(project, target)] as const,
|
|
733
735
|
),
|
|
736
|
+
),
|
|
737
|
+
)
|
|
738
|
+
const targets = (
|
|
739
|
+
await Promise.all(
|
|
740
|
+
[...resolvedTargets.values()].map((target) => assertSafePlannedTarget(project, target)),
|
|
734
741
|
)
|
|
735
742
|
).filter((target, index, all) => all.indexOf(target) === index)
|
|
736
743
|
const invocation = shadcnInvocation(project.manager, release.compatibility.shadcn, [
|
|
@@ -802,7 +809,9 @@ export async function addUi(
|
|
|
802
809
|
sources: itemDocuments.map((item) => ({
|
|
803
810
|
address: item.meta.canonicalAddress,
|
|
804
811
|
dependencies: item.dependencies ?? [],
|
|
805
|
-
files: item.files
|
|
812
|
+
files: item.files
|
|
813
|
+
.map((file) => (file.target ? resolvedTargets.get(file.target) : undefined))
|
|
814
|
+
.filter(Boolean),
|
|
806
815
|
})),
|
|
807
816
|
command: [invocation.file, ...invocation.args],
|
|
808
817
|
output: result.stdout,
|
|
@@ -824,7 +833,7 @@ export async function addUi(
|
|
|
824
833
|
const files: Record<string, string> = {}
|
|
825
834
|
for (const file of item.files) {
|
|
826
835
|
if (!file.target) continue
|
|
827
|
-
const target = await safeTarget(project, file.target)
|
|
836
|
+
const target = await safeTarget(project, resolvedTargets.get(file.target)!)
|
|
828
837
|
files[projectRelative(project, target)] = digest(await readFile(target))
|
|
829
838
|
}
|
|
830
839
|
lock.items[item.meta.canonicalAddress] = {
|
package/src/ui/project.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, lstat, readFile, realpath } from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { loadConfig } from 'tsconfig-paths'
|
|
3
4
|
|
|
4
5
|
import { UiError, type PackageManager } from './model'
|
|
5
6
|
|
|
@@ -40,6 +41,119 @@ async function readManifest(target: string): Promise<Record<string, unknown>> {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function pathPatternMatch(pattern: string, candidate: string): string | undefined {
|
|
45
|
+
const wildcard = pattern.indexOf('*')
|
|
46
|
+
if (wildcard === -1) return pattern === candidate ? '' : undefined
|
|
47
|
+
const prefix = pattern.slice(0, wildcard)
|
|
48
|
+
const suffix = pattern.slice(wildcard + 1)
|
|
49
|
+
if (!candidate.startsWith(prefix) || !candidate.endsWith(suffix)) return undefined
|
|
50
|
+
return candidate.slice(prefix.length, candidate.length - suffix.length)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolvePackageImport(project: UiProject, candidate: string): string | undefined {
|
|
54
|
+
const imports = project.packageJson.imports
|
|
55
|
+
if (!imports || typeof imports !== 'object' || Array.isArray(imports)) return undefined
|
|
56
|
+
const matches = Object.entries(imports as Record<string, unknown>)
|
|
57
|
+
.flatMap(([pattern, target]) => {
|
|
58
|
+
const wildcard = pathPatternMatch(pattern, candidate)
|
|
59
|
+
if (wildcard === undefined || typeof target !== 'string' || !target.startsWith('./'))
|
|
60
|
+
return []
|
|
61
|
+
const wildcardIndex = pattern.indexOf('*')
|
|
62
|
+
return [
|
|
63
|
+
{
|
|
64
|
+
exact: wildcardIndex === -1,
|
|
65
|
+
prefixLength: wildcardIndex === -1 ? pattern.length : wildcardIndex,
|
|
66
|
+
suffixLength: wildcardIndex === -1 ? 0 : pattern.length - wildcardIndex - 1,
|
|
67
|
+
target: target.replaceAll('*', wildcard),
|
|
68
|
+
},
|
|
69
|
+
]
|
|
70
|
+
})
|
|
71
|
+
.sort(
|
|
72
|
+
(left, right) =>
|
|
73
|
+
Number(right.exact) - Number(left.exact) ||
|
|
74
|
+
right.prefixLength - left.prefixLength ||
|
|
75
|
+
right.suffixLength - left.suffixLength,
|
|
76
|
+
)
|
|
77
|
+
return matches[0] ? path.resolve(project.root, matches[0].target) : undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolveAlias(project: UiProject, candidate: string): Promise<string | undefined> {
|
|
81
|
+
const config = loadConfig(project.root)
|
|
82
|
+
if (config.resultType === 'failed') return undefined
|
|
83
|
+
const matches: Array<{
|
|
84
|
+
exact: boolean
|
|
85
|
+
prefixLength: number
|
|
86
|
+
suffixLength: number
|
|
87
|
+
resolved: string
|
|
88
|
+
}> = []
|
|
89
|
+
for (const [pattern, replacements] of Object.entries(config.paths)) {
|
|
90
|
+
const wildcard = pathPatternMatch(pattern, candidate)
|
|
91
|
+
if (wildcard === undefined) continue
|
|
92
|
+
const replacement = replacements?.[0]
|
|
93
|
+
if (!replacement) continue
|
|
94
|
+
const wildcardIndex = pattern.indexOf('*')
|
|
95
|
+
matches.push({
|
|
96
|
+
exact: wildcardIndex === -1,
|
|
97
|
+
prefixLength: wildcardIndex === -1 ? pattern.length : wildcardIndex,
|
|
98
|
+
suffixLength: wildcardIndex === -1 ? 0 : pattern.length - wildcardIndex - 1,
|
|
99
|
+
resolved: path.resolve(config.absoluteBaseUrl, replacement.replaceAll('*', wildcard)),
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
matches.sort(
|
|
103
|
+
(left, right) =>
|
|
104
|
+
Number(right.exact) - Number(left.exact) ||
|
|
105
|
+
right.prefixLength - left.prefixLength ||
|
|
106
|
+
right.suffixLength - left.suffixLength,
|
|
107
|
+
)
|
|
108
|
+
const best = matches[0]
|
|
109
|
+
if (!best) return undefined
|
|
110
|
+
const ambiguous = matches.some(
|
|
111
|
+
(match) =>
|
|
112
|
+
match.exact === best.exact &&
|
|
113
|
+
match.prefixLength === best.prefixLength &&
|
|
114
|
+
match.suffixLength === best.suffixLength &&
|
|
115
|
+
match.resolved !== best.resolved,
|
|
116
|
+
)
|
|
117
|
+
if (ambiguous) {
|
|
118
|
+
throw new UiError(
|
|
119
|
+
'UI_PROJECT_UNSUPPORTED',
|
|
120
|
+
'The components alias resolves to conflicting project paths.',
|
|
121
|
+
'Keep one authoritative compilerOptions.paths mapping for the components alias.',
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
return best.resolved
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function resolveUiRegistryTarget(
|
|
128
|
+
project: UiProject,
|
|
129
|
+
declaredTarget: string,
|
|
130
|
+
): Promise<string> {
|
|
131
|
+
if (!declaredTarget.startsWith('components/')) return declaredTarget
|
|
132
|
+
const components = await readFile(project.componentsPath, 'utf8')
|
|
133
|
+
.then((value) => JSON.parse(value) as { aliases?: { components?: unknown } })
|
|
134
|
+
.catch(() => undefined)
|
|
135
|
+
const componentsAlias = components?.aliases?.components
|
|
136
|
+
if (typeof componentsAlias !== 'string' || componentsAlias.length === 0) return declaredTarget
|
|
137
|
+
const suffix = declaredTarget.slice('components/'.length)
|
|
138
|
+
const directAlias = /^(?:\.?\.?\/|src\/|app\/|frontend\/|components\/)/u.test(componentsAlias)
|
|
139
|
+
const resolved =
|
|
140
|
+
resolvePackageImport(project, componentsAlias) ??
|
|
141
|
+
(await resolveAlias(project, componentsAlias)) ??
|
|
142
|
+
(directAlias ? path.resolve(project.root, componentsAlias) : undefined)
|
|
143
|
+
if (!resolved) {
|
|
144
|
+
throw new UiError(
|
|
145
|
+
'UI_PROJECT_UNSUPPORTED',
|
|
146
|
+
'components.json alias cannot be resolved through tsconfig.json or jsconfig.json.',
|
|
147
|
+
'Define a matching compilerOptions.paths entry for ' + componentsAlias + '.',
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
const relative = path.relative(project.root, path.join(resolved, suffix))
|
|
151
|
+
if (relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative)) {
|
|
152
|
+
throw new UiError('UI_PROJECT_UNSUPPORTED', 'components.json alias escapes the project.')
|
|
153
|
+
}
|
|
154
|
+
return relative.split(path.sep).join('/')
|
|
155
|
+
}
|
|
156
|
+
|
|
43
157
|
function hasReactTailwind(manifest: Record<string, unknown>): boolean {
|
|
44
158
|
const dependencies = {
|
|
45
159
|
...(manifest.dependencies as Record<string, string> | undefined),
|