@zenithbuild/cli 0.4.10 → 1.3.4

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": "@zenithbuild/cli",
3
- "version": "0.4.10",
3
+ "version": "1.3.4",
4
4
  "description": "CLI for Zenith framework - dev server, build tools, and plugin management",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,14 +50,16 @@
50
50
  },
51
51
  "private": false,
52
52
  "peerDependencies": {
53
- "@zenithbuild/core": "^1.2.14"
53
+ "@zenithbuild/core": "^1.3.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/bun": "latest"
57
57
  },
58
58
  "dependencies": {
59
- "@zenithbuild/compiler": "^1.0.15",
60
- "@zenithbuild/router": "^1.0.7",
59
+ "@zenithbuild/compiler": "1.3.6",
60
+ "@zenithbuild/bundler": "1.3.4",
61
+ "@zenithbuild/router": "1.3.0",
62
+ "glob": "^13.0.0",
61
63
  "picocolors": "^1.0.0"
62
64
  }
63
65
  }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { requireProject } from '../utils/project'
8
8
  import * as logger from '../utils/logger'
9
- import { buildSSG } from '@zenithbuild/compiler'
9
+ import { buildSSG } from '../../../zenith-compiler/dist/index.js'
10
10
 
11
11
  export interface BuildOptions {
12
12
  outDir?: string
@@ -24,24 +24,26 @@ import { serve, type ServerWebSocket } from 'bun'
24
24
  import { requireProject } from '../utils/project'
25
25
  import * as logger from '../utils/logger'
26
26
  import * as brand from '../utils/branding'
27
+ import { generateRuntime, type ZenManifest } from '@zenithbuild/bundler'
28
+ import { compile } from '@zenithbuild/compiler'
29
+ import { discoverLayouts } from '@zenithbuild/compiler/layouts'
30
+ import { processLayout } from '@zenithbuild/compiler/transform'
31
+ import { generateBundleJS } from '@zenithbuild/compiler/runtime'
32
+ import { loadZenithConfig } from '@zenithbuild/compiler/config'
27
33
  import {
28
- compileZenSource,
29
- discoverLayouts,
30
- processLayout,
31
- generateBundleJS,
32
- loadZenithConfig,
33
34
  PluginRegistry,
34
35
  createPluginContext,
35
- getPluginDataByNamespace,
36
- compileCssAsync,
37
- resolveGlobalsCss,
36
+ getPluginDataByNamespace
37
+ } from '@zenithbuild/compiler/registry'
38
+ import { compileCssAsync, resolveGlobalsCss } from '@zenithbuild/compiler/css'
39
+ import {
38
40
  createBridgeAPI,
39
41
  runPluginHooks,
40
42
  collectHookReturns,
41
43
  buildRuntimeEnvelope,
42
- clearHooks,
43
- type HookContext
44
- } from '@zenithbuild/compiler'
44
+ clearHooks
45
+ } from '@zenithbuild/compiler/plugins'
46
+ import type { HookContext } from '@zenithbuild/compiler/plugins'
45
47
 
46
48
  export interface DevOptions {
47
49
  port?: number
@@ -61,7 +63,9 @@ const pageCache = new Map<string, CompiledPage>()
61
63
  * Bundle page script using Rolldown to resolve npm imports at compile time.
62
64
  * Only called when compiler emits a BundlePlan - bundler performs no inference.
63
65
  */
64
- import { bundlePageScript, type BundlePlan, generateRouteDefinition } from '@zenithbuild/compiler'
66
+ import { bundlePageScript } from '@zenithbuild/compiler/bundler'
67
+ import { generateRouteDefinition } from '@zenithbuild/compiler/bundler'
68
+ import type { BundlePlan } from '@zenithbuild/compiler'
65
69
 
66
70
  export async function dev(options: DevOptions = {}): Promise<void> {
67
71
  const project = requireProject()
@@ -155,86 +159,23 @@ export async function dev(options: DevOptions = {}): Promise<void> {
155
159
  const layouts = discoverLayouts(layoutsDir)
156
160
  const source = fs.readFileSync(pagePath, 'utf-8')
157
161
 
158
- let processedSource = source
159
- let layoutToUse = layouts.get('DefaultLayout')
160
-
161
- if (layoutToUse) processedSource = processLayout(source, layoutToUse)
162
-
163
- const result = await compileZenSource(processedSource, pagePath, {
164
- componentsDir: fs.existsSync(componentsDir) ? componentsDir : undefined
162
+ const result = await compile(source, pagePath, {
163
+ componentsDir: fs.existsSync(componentsDir) ? componentsDir : undefined,
164
+ components: layouts
165
165
  })
166
- if (!result.finalized) throw new Error('Compilation failed')
166
+ if (!result.finalized || !result.finalized.manifest) throw new Error('Compilation failed')
167
167
 
168
168
  const routeDef = generateRouteDefinition(pagePath, pagesDir)
169
169
 
170
-
171
-
172
- // Safely strip imports from the top of the script
173
- // This relies on the fact that duplicate imports (from Rust codegen)
174
- // appear at the beginning of result.finalized.js
175
- let jsLines = result.finalized.js.split('\n')
176
-
177
- // Remove lines from top that are imports, whitespace, or comments
178
- while (jsLines.length > 0 && jsLines[0] !== undefined) {
179
- const line = jsLines[0].trim()
180
- if (
181
- line.startsWith('import ') ||
182
- line === '' ||
183
- line.startsWith('//') ||
184
- line.startsWith('/*') ||
185
- line.startsWith('*')
186
- ) {
187
- jsLines.shift()
188
- } else {
189
- break
190
- }
191
- }
192
-
193
- let jsWithoutImports = jsLines.join('\n')
194
-
195
- // PATCH: Fix unquoted keys with dashes (Rust codegen bug in jsx_lowerer)
196
- // e.g. stroke-width: "1.5" -> "stroke-width": "1.5"
197
- // We only apply this to the JS portions (Script and Expressions)
198
- // to avoid corrupting the Styles section.
199
- const stylesMarker = '// 6. Styles injection'
200
- const parts = jsWithoutImports.split(stylesMarker)
201
-
202
- if (parts.length > 1) {
203
- // Apply patch only to the JS part
204
- parts[0] = parts[0]!.replace(
205
- /(^|[{,])\s*([a-zA-Z][a-zA-Z0-9-]*-[a-zA-Z0-9-]*)\s*:/gm,
206
- '$1"$2":'
207
- )
208
- jsWithoutImports = parts.join(stylesMarker)
209
- } else {
210
- // Fallback if marker not found
211
- jsWithoutImports = jsWithoutImports.replace(
212
- /(^|[{,])\s*([a-zA-Z][a-zA-Z0-9-]*-[a-zA-Z0-9-]*)\s*:/gm,
213
- '$1"$2":'
214
- )
215
- }
216
-
217
- // Combine: structured imports first, then cleaned script body
218
- const fullScript = (result.finalized.npmImports || '') + '\n\n' + jsWithoutImports
219
-
220
-
221
-
222
- // Bundle ONLY if compiler emitted a BundlePlan (no inference)
223
- let bundledScript = fullScript
224
- if (result.finalized.bundlePlan) {
225
- // Compiler decided bundling is needed - pass plan with proper resolve roots
226
- const plan: BundlePlan = {
227
- ...result.finalized.bundlePlan,
228
- entry: fullScript,
229
- resolveRoots: [path.join(rootDir, 'node_modules'), 'node_modules']
230
- }
231
- bundledScript = await bundlePageScript(plan)
232
- }
170
+ // Use the new bundler to generate the runtime + author script
171
+ // This replaces all the manual regex patching and string concatenation
172
+ const manifest: ZenManifest = result.finalized.manifest
173
+ const { code } = generateRuntime(manifest, true)
233
174
 
234
175
  return {
235
176
  html: result.finalized.html,
236
- script: bundledScript,
237
- styles: result.finalized.styles,
177
+ script: code,
178
+ styles: [], // Styles are now injected via the script
238
179
  route: routeDef.path,
239
180
  lastModified: Date.now()
240
181
  }
@@ -245,22 +186,15 @@ export async function dev(options: DevOptions = {}): Promise<void> {
245
186
  }
246
187
 
247
188
  /**
248
- * Generate dev HTML with plugin data envelope
249
- *
250
- * CLI collects payloads from plugins via 'cli:runtime:collect' hook.
251
- * It serializes blindly - never inspecting what's inside.
189
+ * Generate dev HTML
252
190
  */
253
191
  async function generateDevHTML(page: CompiledPage): Promise<string> {
254
- // Single neutral injection point
255
- const runtimeTag = `<script src="/runtime.js"></script>`
256
192
  const scriptTag = `<script type="module">\n${page.script}\n</script>`
257
- const allScripts = `${runtimeTag}\n${scriptTag}`
258
193
 
259
194
  let html = page.html.includes('</body>')
260
- ? page.html.replace('</body>', `${allScripts}\n</body>`)
261
- : `${page.html}\n${allScripts}`
195
+ ? page.html.replace('</body>', `${scriptTag}\n</body>`)
196
+ : `${page.html}\n${scriptTag}`
262
197
 
263
- // Ensure DOCTYPE is present to prevent Quirks Mode (critical for SVG namespace)
264
198
  if (!html.trimStart().toLowerCase().startsWith('<!doctype')) {
265
199
  html = `<!DOCTYPE html>\n${html}`
266
200
  }
@@ -10,6 +10,7 @@ import { build, type BuildOptions } from './build'
10
10
  import { add, type AddOptions } from './add'
11
11
  import { remove } from './remove'
12
12
  import { create } from './create'
13
+ import { lint, type LintOptions } from './lint'
13
14
  import * as logger from '../utils/logger'
14
15
 
15
16
  export interface Command {
@@ -84,6 +85,17 @@ export const commands: Command[] = [
84
85
  }
85
86
  await remove(pluginName)
86
87
  }
88
+ },
89
+ {
90
+ name: 'lint',
91
+ description: 'Audit project for Zenith Contract compliance',
92
+ usage: 'zenith lint [--fix]',
93
+ async run(args, options) {
94
+ const opts: LintOptions = {}
95
+ if (options.fix) opts.fix = true
96
+ if (options.incremental) opts.incremental = true
97
+ await lint(args, opts)
98
+ }
87
99
  }
88
100
  ]
89
101
 
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @zenithbuild/cli - Lint Command
3
+ *
4
+ * Scans for .zen files and enforces the Zenith Contract.
5
+ */
6
+
7
+ import { glob } from 'glob'
8
+ import { join } from 'path'
9
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'
10
+ import { compile } from '../../../zenith-compiler/dist/index.js'
11
+ import * as logger from '../utils/logger'
12
+ import picocolors from 'picocolors'
13
+ import { createHash } from 'crypto'
14
+ import { dirname } from 'path'
15
+
16
+ export interface LintOptions {
17
+ fix?: boolean
18
+ incremental?: boolean
19
+ }
20
+
21
+ interface LintCache {
22
+ files: Record<string, string> // path -> hash
23
+ }
24
+
25
+ function findProjectRoot(startDir: string): string {
26
+ let current = startDir
27
+ while (current !== dirname(current)) {
28
+ if (existsSync(join(current, 'package.json')) || existsSync(join(current, '.zenith'))) {
29
+ return current
30
+ }
31
+ current = dirname(current)
32
+ }
33
+ return startDir
34
+ }
35
+
36
+ export async function lint(fileArgs: string[] = [], options: LintOptions = {}): Promise<void> {
37
+ logger.header('Zenith Safety Audit')
38
+
39
+ const isIncremental = options.incremental || false
40
+
41
+ // Find files
42
+ // Filter out flags that might have been passed in args
43
+ const filesToScan = fileArgs.filter(a => !a.startsWith('--'))
44
+ let files: string[] = []
45
+
46
+ if (filesToScan.length > 0) {
47
+ files = filesToScan
48
+ } else {
49
+ files = await glob('**/*.zen', {
50
+ ignore: ['**/node_modules/**', '**/dist/**', '.git/**', '**/test/**']
51
+ })
52
+ }
53
+
54
+ if (files.length === 0) {
55
+ logger.info('No .zen files found to audit.')
56
+ return
57
+ }
58
+
59
+ // Load lint cache - Optimized for monorepos
60
+ const root = findProjectRoot(process.cwd())
61
+ const cacheDir = join(root, '.zenith/cache')
62
+ const cachePath = join(cacheDir, 'lint-cache.json')
63
+ let lintCache: LintCache = { files: {} }
64
+
65
+ if (isIncremental && existsSync(cachePath)) {
66
+ try {
67
+ lintCache = JSON.parse(readFileSync(cachePath, 'utf-8'))
68
+ } catch (e) {
69
+ logger.warn('Failed to load lint cache, starting fresh.')
70
+ }
71
+ }
72
+
73
+ logger.info(`Auditing ${files.length} components${isIncremental ? ' (incremental)' : ''}...\n`)
74
+
75
+ let errorCount = 0
76
+ let fileCount = 0
77
+ let skippedCount = 0
78
+ const newCache: LintCache = { files: { ...lintCache.files } }
79
+
80
+ for (const file of files) {
81
+ fileCount++
82
+ const source = readFileSync(file, 'utf-8')
83
+ const hash = createHash('sha256').update(source).digest('hex')
84
+
85
+ if (isIncremental && lintCache.files[file] === hash) {
86
+ skippedCount++
87
+ continue
88
+ }
89
+
90
+ try {
91
+ // Run compilation with cache enabled for components
92
+ process.env.ZENITH_CACHE = '1'
93
+ await compile(source, file)
94
+ newCache.files[file] = hash
95
+ } catch (error: any) {
96
+ errorCount++
97
+ // Clear cache for this file on error
98
+ delete newCache.files[file]
99
+
100
+ if (error.name === 'InvariantError' || error.name === 'CompilerError') {
101
+ console.log(picocolors.red(picocolors.bold(`\n✖ ${file}:${error.line}:${error.column}`)))
102
+ console.log(picocolors.red(` [${error.code || 'ERROR'}] ${error.message}`))
103
+
104
+ if (error.guarantee) {
105
+ console.log(picocolors.yellow(` Guarantee: ${error.guarantee}`))
106
+ }
107
+
108
+ if (error.context) {
109
+ console.log(picocolors.dim(` Context: ${error.context}`))
110
+ }
111
+
112
+ if (error.hints && error.hints.length > 0) {
113
+ console.log(picocolors.cyan(' Hints:'))
114
+ for (const hint of error.hints) {
115
+ console.log(picocolors.cyan(` - ${hint}`))
116
+ }
117
+ }
118
+ } else {
119
+ console.log(picocolors.red(`\n✖ ${file}: Unexpected error`))
120
+ console.log(picocolors.red(` ${error.message}`))
121
+ }
122
+ }
123
+ }
124
+
125
+ // Save updated cache
126
+ if (isIncremental) {
127
+ if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true })
128
+ writeFileSync(cachePath, JSON.stringify(newCache, null, 2))
129
+ }
130
+
131
+ console.log('')
132
+
133
+ if (errorCount > 0) {
134
+ logger.error(`Audit failed. Found ${errorCount} contract violations in ${fileCount} files.`)
135
+ process.exit(1)
136
+ } else {
137
+ const skipMsg = skippedCount > 0 ? ` (${skippedCount} unchanged files skipped)` : ''
138
+ logger.success(`Audit passed. ${fileCount} files checked${skipMsg}, 0 violations found.`)
139
+ }
140
+ }