@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.
Files changed (33) hide show
  1. package/dist/astrale.js +2759 -625
  2. package/dist/public/connect-core.js +4 -4
  3. package/dist/types/admin/contract.d.ts +2 -2
  4. package/package.json +5 -4
  5. package/src/admin/contract.ts +3 -2
  6. package/src/commands/__tests__/read-commands.test.ts +56 -0
  7. package/src/commands/__tests__/view.test.ts +1 -2
  8. package/src/commands/introspect.ts +8 -9
  9. package/src/ui/.spec/architecture.md +8 -7
  10. package/src/ui/.spec/laws.ts +7 -0
  11. package/src/ui/__tests__/ui.test.ts +357 -5
  12. package/src/ui/lock.ts +1 -1
  13. package/src/ui/operations.ts +89 -19
  14. package/src/ui/project.ts +114 -0
  15. package/src/ui/release.ts +9 -2
  16. package/studio/client/dist/assets/{index-7YHdeCxp.js → index-OeJK1TjG.js} +2 -2
  17. package/studio/client/dist/assets/{schema-studio-CABpTfvH.js → schema-studio-DRdfu_cQ.js} +2 -2
  18. package/studio/client/dist/index.html +1 -1
  19. package/studio/package.json +3 -3
  20. package/studio/server/cli-consumers.test.ts +0 -1
  21. package/studio/server/introspect/anatomy/views.ts +1 -2
  22. package/studio/server/introspect/anatomy-extras.test.ts +3 -14
  23. package/studio/server/introspect/canonical-schema.test.ts +1 -1
  24. package/studio/server/introspect/canonical-schema.ts +0 -4
  25. package/studio/server/introspect/diff.test.ts +10 -2
  26. package/studio/server/introspect/diff.ts +1 -9
  27. package/studio/server/introspect/runtime.test.ts +1 -1
  28. package/studio/server/introspect/schema-ir-json.test.ts +12 -1
  29. package/studio/server/introspect/schema-ir-json.ts +1 -1
  30. package/studio/server/views/target.test.ts +1 -2
  31. package/studio/shared/contracts/schema.ts +1 -3
  32. package/studio/tsconfig.json +2 -1
  33. package/viewer/dist/main.js +13 -13
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),
package/src/ui/release.ts CHANGED
@@ -250,8 +250,14 @@ function isInstallableItem(item: unknown): item is UiRegistry['items'][number] {
250
250
  /^(?:pattern|block)-[a-z0-9-]+$/u.test(candidate.name) &&
251
251
  typeof address === 'string' &&
252
252
  /^(?:pattern|block)\/[a-z0-9-]+\/[a-z0-9-/]+$/u.test(address)
253
+ const isComponent =
254
+ candidate.type === 'registry:component' &&
255
+ typeof candidate.name === 'string' &&
256
+ /^component-[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(candidate.name) &&
257
+ typeof address === 'string' &&
258
+ /^component\/[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(address)
253
259
  return (
254
- (isTheme || isComposition) &&
260
+ (isTheme || isComposition || isComponent) &&
255
261
  (candidate.dependencies === undefined ||
256
262
  (Array.isArray(candidate.dependencies) &&
257
263
  candidate.dependencies.every(
@@ -274,7 +280,8 @@ function isInstallableItem(item: unknown): item is UiRegistry['items'][number] {
274
280
  (candidate.files?.length === 1 &&
275
281
  file.type === 'registry:file' &&
276
282
  file.target ===
277
- 'components/astrale/theme/' + address!.slice('theme/'.length) + '.css')),
283
+ 'components/astrale/theme/' + address!.slice('theme/'.length) + '.css')) &&
284
+ (!isComponent || file.target.startsWith('components/astrale/component/')),
278
285
  ) &&
279
286
  typeof address === 'string'
280
287
  )