@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/dist/zen-build.js +9886 -1984
- package/dist/zen-dev.js +9886 -1984
- package/dist/zen-preview.js +9886 -1984
- package/dist/zenith.js +9886 -1984
- package/package.json +6 -4
- package/src/commands/build.ts +1 -1
- package/src/commands/dev.ts +29 -95
- package/src/commands/index.ts +12 -0
- package/src/commands/lint.ts +140 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenithbuild/cli",
|
|
3
|
-
"version": "
|
|
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.
|
|
53
|
+
"@zenithbuild/core": "^1.3.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/bun": "latest"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@zenithbuild/compiler": "
|
|
60
|
-
"@zenithbuild/
|
|
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
|
}
|
package/src/commands/build.ts
CHANGED
|
@@ -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 '
|
|
9
|
+
import { buildSSG } from '../../../zenith-compiler/dist/index.js'
|
|
10
10
|
|
|
11
11
|
export interface BuildOptions {
|
|
12
12
|
outDir?: string
|
package/src/commands/dev.ts
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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:
|
|
237
|
-
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
|
|
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>', `${
|
|
261
|
-
: `${page.html}\n${
|
|
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
|
}
|
package/src/commands/index.ts
CHANGED
|
@@ -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
|
+
}
|