uniweb 0.3.4 → 0.3.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,9 +37,9 @@
37
37
  "js-yaml": "^4.1.0",
38
38
  "prompts": "^2.4.2",
39
39
  "tar": "^7.0.0",
40
- "@uniweb/build": "0.2.2",
41
- "@uniweb/core": "0.2.2",
42
- "@uniweb/runtime": "0.3.0",
43
- "@uniweb/kit": "0.2.1"
40
+ "@uniweb/build": "0.2.3",
41
+ "@uniweb/core": "0.2.3",
42
+ "@uniweb/runtime": "0.3.1",
43
+ "@uniweb/kit": "0.2.2"
44
44
  }
45
45
  }
@@ -311,12 +311,39 @@ async function generateLocalizedHtml(projectDir, i18nConfig) {
311
311
  /**
312
312
  * Resolve foundation directory based on site config and project structure
313
313
  *
314
- * For single projects: ../foundation
315
- * For multi projects: ../../foundations/{name}
314
+ * Priority:
315
+ * 1. Read site's package.json to find foundation dependency path (most reliable)
316
+ * 2. Check foundations/{name} for multi-site projects
317
+ * 3. Check ../foundation for single-site projects
318
+ * 4. Check ../{name} as fallback
316
319
  */
317
320
  function resolveFoundationDir(projectDir, siteConfig) {
318
321
  const foundationName = siteConfig?.foundation
319
322
 
323
+ // First, try to resolve from site's package.json dependencies
324
+ // This is the most reliable method as it matches how Vite resolves imports
325
+ if (foundationName) {
326
+ const pkgPath = join(projectDir, 'package.json')
327
+ if (existsSync(pkgPath)) {
328
+ try {
329
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
330
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
331
+ const depValue = deps[foundationName]
332
+
333
+ // Check for file: protocol (local dependency)
334
+ if (depValue && depValue.startsWith('file:')) {
335
+ const relativePath = depValue.slice(5) // Remove 'file:' prefix
336
+ const resolvedPath = join(projectDir, relativePath)
337
+ if (existsSync(resolvedPath)) {
338
+ return resolvedPath
339
+ }
340
+ }
341
+ } catch {
342
+ // Ignore JSON parse errors, fall through to other methods
343
+ }
344
+ }
345
+ }
346
+
320
347
  // Check if we're in a multi-site structure (site is under sites/)
321
348
  const parentDir = join(projectDir, '..')
322
349
  const grandParentDir = join(projectDir, '..', '..')
@@ -416,6 +443,20 @@ async function buildSite(projectDir, options = {}) {
416
443
  }
417
444
  } catch (err) {
418
445
  error(`Pre-rendering failed: ${err.message}`)
446
+
447
+ // Provide helpful guidance for common errors
448
+ if (err.message.includes('Foundation not found')) {
449
+ log('')
450
+ log(`${colors.yellow}This usually means:${colors.reset}`)
451
+ log(` 1. The foundation hasn't been built yet (run foundation build first)`)
452
+ log(` 2. The foundation name in site.yml doesn't match your setup`)
453
+ log('')
454
+ log(`${colors.dim}Check that:${colors.reset}`)
455
+ log(` • site.yml 'foundation:' matches the package name in your foundation's package.json`)
456
+ log(` • site's package.json has a dependency pointing to the correct foundation path`)
457
+ log(` • The foundation's dist/foundation.js exists (build the foundation first)`)
458
+ }
459
+
419
460
  if (process.env.DEBUG) {
420
461
  console.error(err.stack)
421
462
  }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * uniweb doctor - Diagnose project configuration issues
3
+ */
4
+
5
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
6
+ import { join, resolve, basename, dirname, relative } from 'node:path'
7
+ import yaml from 'js-yaml'
8
+
9
+ // ANSI colors
10
+ const colors = {
11
+ reset: '\x1b[0m',
12
+ bright: '\x1b[1m',
13
+ dim: '\x1b[2m',
14
+ red: '\x1b[31m',
15
+ green: '\x1b[32m',
16
+ yellow: '\x1b[33m',
17
+ blue: '\x1b[36m'
18
+ }
19
+
20
+ const success = (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`)
21
+ const warn = (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`)
22
+ const error = (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`)
23
+ const info = (msg) => console.log(`${colors.blue}→${colors.reset} ${msg}`)
24
+ const log = console.log
25
+
26
+ /**
27
+ * Check if a directory is a site
28
+ */
29
+ function isSite(dir) {
30
+ return existsSync(join(dir, 'site.yml')) || existsSync(join(dir, 'site.yaml'))
31
+ }
32
+
33
+ /**
34
+ * Check if a directory is a foundation
35
+ */
36
+ function isFoundation(dir) {
37
+ return existsSync(join(dir, 'src', 'components'))
38
+ }
39
+
40
+ /**
41
+ * Load package.json from a directory
42
+ */
43
+ function loadPackageJson(dir) {
44
+ const pkgPath = join(dir, 'package.json')
45
+ if (!existsSync(pkgPath)) return null
46
+ try {
47
+ return JSON.parse(readFileSync(pkgPath, 'utf8'))
48
+ } catch {
49
+ return null
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Load site.yml from a directory
55
+ */
56
+ function loadSiteYml(dir) {
57
+ const ymlPath = join(dir, 'site.yml')
58
+ const yamlPath = join(dir, 'site.yaml')
59
+ const configPath = existsSync(ymlPath) ? ymlPath : existsSync(yamlPath) ? yamlPath : null
60
+ if (!configPath) return null
61
+ try {
62
+ return yaml.load(readFileSync(configPath, 'utf8'))
63
+ } catch {
64
+ return null
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Main doctor command
70
+ */
71
+ export async function doctor(args = []) {
72
+ log('')
73
+ log(`${colors.blue}${colors.bright}Uniweb Doctor${colors.reset}`)
74
+ log(`${colors.dim}Checking project configuration...${colors.reset}`)
75
+
76
+ const projectDir = resolve(process.cwd())
77
+
78
+ // Detect project type and find workspace root
79
+ let workspaceDir = projectDir
80
+
81
+ if (isSite(projectDir)) {
82
+ workspaceDir = dirname(projectDir)
83
+ if (basename(workspaceDir) === 'sites') {
84
+ workspaceDir = dirname(workspaceDir)
85
+ }
86
+ } else if (isFoundation(projectDir)) {
87
+ workspaceDir = dirname(projectDir)
88
+ if (basename(workspaceDir) === 'foundations') {
89
+ workspaceDir = dirname(workspaceDir)
90
+ }
91
+ }
92
+
93
+ // Check workspace structure
94
+ const hasWorkspaceConfig = existsSync(join(workspaceDir, 'pnpm-workspace.yaml')) ||
95
+ existsSync(join(workspaceDir, 'package.json'))
96
+
97
+ if (!hasWorkspaceConfig) {
98
+ error('Not in a Uniweb workspace')
99
+ log(`${colors.dim}Run this command from your project root or a site/foundation directory.${colors.reset}`)
100
+ process.exit(1)
101
+ }
102
+
103
+ log('')
104
+ info(`Workspace: ${workspaceDir}`)
105
+
106
+ // Find all foundations
107
+ const foundations = []
108
+
109
+ // Check single-foundation layout
110
+ const foundationDir = join(workspaceDir, 'foundation')
111
+ if (isFoundation(foundationDir)) {
112
+ const pkg = loadPackageJson(foundationDir)
113
+ foundations.push({
114
+ path: foundationDir,
115
+ name: pkg?.name || 'foundation',
116
+ folderName: 'foundation'
117
+ })
118
+ }
119
+
120
+ // Check multi-foundation layout
121
+ const foundationsDir = join(workspaceDir, 'foundations')
122
+ if (existsSync(foundationsDir)) {
123
+ try {
124
+ const entries = readdirSync(foundationsDir, { withFileTypes: true })
125
+ for (const entry of entries) {
126
+ if (entry.isDirectory()) {
127
+ const foundationPath = join(foundationsDir, entry.name)
128
+ if (isFoundation(foundationPath)) {
129
+ const pkg = loadPackageJson(foundationPath)
130
+ foundations.push({
131
+ path: foundationPath,
132
+ name: pkg?.name || entry.name,
133
+ folderName: entry.name
134
+ })
135
+ }
136
+ }
137
+ }
138
+ } catch {
139
+ // Ignore errors
140
+ }
141
+ }
142
+
143
+ if (foundations.length === 0) {
144
+ warn('No foundations found')
145
+ } else {
146
+ success(`Found ${foundations.length} foundation(s):`)
147
+ for (const f of foundations) {
148
+ const nameMismatch = f.name !== f.folderName ? ` ${colors.dim}(folder: ${f.folderName}/)${colors.reset}` : ''
149
+ log(` • ${f.name}${nameMismatch}`)
150
+ }
151
+ }
152
+
153
+ // Find all sites
154
+ const sites = []
155
+
156
+ // Check single-site layout
157
+ const siteDir = join(workspaceDir, 'site')
158
+ if (isSite(siteDir)) {
159
+ sites.push({ path: siteDir, name: 'site' })
160
+ }
161
+
162
+ // Check multi-site layout
163
+ const sitesDir = join(workspaceDir, 'sites')
164
+ if (existsSync(sitesDir)) {
165
+ try {
166
+ const entries = readdirSync(sitesDir, { withFileTypes: true })
167
+ for (const entry of entries) {
168
+ if (entry.isDirectory()) {
169
+ const sitePath = join(sitesDir, entry.name)
170
+ if (isSite(sitePath)) {
171
+ sites.push({ path: sitePath, name: entry.name })
172
+ }
173
+ }
174
+ }
175
+ } catch {
176
+ // Ignore errors
177
+ }
178
+ }
179
+
180
+ if (sites.length === 0) {
181
+ warn('No sites found')
182
+ } else {
183
+ log('')
184
+ success(`Found ${sites.length} site(s):`)
185
+ for (const s of sites) {
186
+ log(` • ${s.name}`)
187
+ }
188
+ }
189
+
190
+ // Check each site
191
+ const issues = []
192
+
193
+ for (const site of sites) {
194
+ const siteName = site.name
195
+ const sitePath = site.path
196
+ const siteYml = loadSiteYml(sitePath)
197
+ const sitePkg = loadPackageJson(sitePath)
198
+
199
+ log('')
200
+ info(`Checking site: ${siteName}`)
201
+
202
+ if (!siteYml) {
203
+ issues.push({ type: 'error', site: siteName, message: 'Missing site.yml' })
204
+ error('Missing site.yml')
205
+ continue
206
+ }
207
+
208
+ if (!sitePkg) {
209
+ issues.push({ type: 'error', site: siteName, message: 'Missing package.json' })
210
+ error('Missing package.json')
211
+ continue
212
+ }
213
+
214
+ const foundationName = siteYml.foundation
215
+ if (!foundationName) {
216
+ warn('No foundation specified in site.yml (using runtime loading?)')
217
+ continue
218
+ }
219
+
220
+ // Check if foundation name matches a known foundation
221
+ const matchingFoundation = foundations.find(f => f.name === foundationName)
222
+
223
+ if (!matchingFoundation) {
224
+ // Check if it might match a folder name instead
225
+ const folderMatch = foundations.find(f => f.folderName === foundationName)
226
+ if (folderMatch) {
227
+ issues.push({
228
+ type: 'error',
229
+ site: siteName,
230
+ message: `Foundation mismatch: site.yml uses folder name "${foundationName}" instead of package name "${folderMatch.name}"`
231
+ })
232
+ error(`Foundation mismatch:`)
233
+ log(` site.yml says: ${colors.yellow}foundation: ${foundationName}${colors.reset}`)
234
+ log(` This matches the folder name, but the package name is: ${colors.green}${folderMatch.name}${colors.reset}`)
235
+ log('')
236
+ log(` ${colors.dim}To fix, update site.yml:${colors.reset}`)
237
+ log(` foundation: ${folderMatch.name}`)
238
+ continue
239
+ }
240
+
241
+ issues.push({
242
+ type: 'error',
243
+ site: siteName,
244
+ message: `Foundation "${foundationName}" not found`
245
+ })
246
+ error(`Foundation "${foundationName}" not found`)
247
+ log('')
248
+ log(` ${colors.dim}Available foundations:${colors.reset}`)
249
+ for (const f of foundations) {
250
+ log(` • ${f.name} (${f.folderName}/)`)
251
+ }
252
+ continue
253
+ }
254
+
255
+ success(`Foundation reference: ${foundationName}`)
256
+
257
+ // Check package.json dependency
258
+ const deps = { ...sitePkg.dependencies, ...sitePkg.devDependencies }
259
+ const depValue = deps[foundationName]
260
+
261
+ if (!depValue) {
262
+ issues.push({
263
+ type: 'error',
264
+ site: siteName,
265
+ message: `Missing dependency "${foundationName}" in package.json`
266
+ })
267
+ error(`Missing dependency "${foundationName}" in package.json`)
268
+ log('')
269
+ log(` ${colors.dim}Add to site's package.json dependencies:${colors.reset}`)
270
+ log(` "${foundationName}": "file:${relative(sitePath, matchingFoundation.path)}"`)
271
+ continue
272
+ }
273
+
274
+ if (depValue.startsWith('file:')) {
275
+ const depPath = join(sitePath, depValue.slice(5))
276
+ if (!existsSync(depPath)) {
277
+ issues.push({
278
+ type: 'error',
279
+ site: siteName,
280
+ message: `Dependency path doesn't exist: ${depValue}`
281
+ })
282
+ error(`Dependency path doesn't exist: ${depValue}`)
283
+ log('')
284
+ log(` ${colors.dim}Update site's package.json:${colors.reset}`)
285
+ log(` "${foundationName}": "file:${relative(sitePath, matchingFoundation.path)}"`)
286
+ continue
287
+ }
288
+ success(`Dependency path: ${depValue}`)
289
+ } else {
290
+ success(`Dependency: ${depValue} (npm package)`)
291
+ }
292
+
293
+ // Check if foundation is built
294
+ const foundationDist = join(matchingFoundation.path, 'dist', 'foundation.js')
295
+ if (!existsSync(foundationDist)) {
296
+ issues.push({
297
+ type: 'warn',
298
+ site: siteName,
299
+ message: `Foundation not built: ${matchingFoundation.name}`
300
+ })
301
+ warn(`Foundation not built yet`)
302
+ log(` ${colors.dim}Run: pnpm --filter ${matchingFoundation.name} build${colors.reset}`)
303
+ } else {
304
+ success(`Foundation built: dist/foundation.js exists`)
305
+ }
306
+ }
307
+
308
+ // Summary
309
+ log('')
310
+ log('─'.repeat(50))
311
+
312
+ const errors = issues.filter(i => i.type === 'error')
313
+ const warnings = issues.filter(i => i.type === 'warn')
314
+
315
+ if (errors.length === 0 && warnings.length === 0) {
316
+ log('')
317
+ log(`${colors.green}${colors.bright}All checks passed!${colors.reset}`)
318
+ log('')
319
+ } else {
320
+ log('')
321
+ if (errors.length > 0) {
322
+ log(`${colors.red}${errors.length} error(s)${colors.reset}`)
323
+ }
324
+ if (warnings.length > 0) {
325
+ log(`${colors.yellow}${warnings.length} warning(s)${colors.reset}`)
326
+ }
327
+ log('')
328
+ }
329
+
330
+ return { issues, errors: errors.length, warnings: warnings.length }
331
+ }
package/src/index.js CHANGED
@@ -18,6 +18,7 @@ import { fileURLToPath } from 'node:url'
18
18
  import prompts from 'prompts'
19
19
  import { build } from './commands/build.js'
20
20
  import { docs } from './commands/docs.js'
21
+ import { doctor } from './commands/doctor.js'
21
22
  import { i18n } from './commands/i18n.js'
22
23
  import { getVersionsForTemplates, getVersion } from './versions.js'
23
24
  import {
@@ -161,6 +162,12 @@ async function main() {
161
162
  return
162
163
  }
163
164
 
165
+ // Handle doctor command
166
+ if (command === 'doctor') {
167
+ await doctor(args.slice(1))
168
+ return
169
+ }
170
+
164
171
  // Handle create command
165
172
  if (command !== 'create') {
166
173
  error(`Unknown command: ${command}`)
@@ -321,6 +328,7 @@ ${colors.bright}Commands:${colors.reset}
321
328
  create [name] Create a new project
322
329
  build Build the current project
323
330
  docs Generate component documentation
331
+ doctor Diagnose project configuration issues
324
332
  i18n <cmd> Internationalization (extract, sync, status)
325
333
 
326
334
  ${colors.bright}Create Options:${colors.reset}