@dinoreic/fez 0.4.0 → 0.5.2

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/bin/fez CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'pathname'
4
4
 
5
+ # Resolve the actual bin directory (handles symlinks from bunx/npx)
6
+ bin_dir = Pathname.new(__FILE__).realpath.dirname
7
+
5
8
  command = ARGV[0]
6
9
  args = ARGV[1..-1]
7
10
 
@@ -10,22 +13,29 @@ if command.nil? || command == '--help' || command == '-h'
10
13
  puts ""
11
14
  puts "Available commands:"
12
15
 
13
- bin_dir = Pathname.new(__FILE__).dirname
14
- commands = Dir[bin_dir.join("fez-*")].map do |path|
16
+ subcommands = Dir[bin_dir.join("fez-*")].map do |path|
15
17
  File.basename(path).sub(/^fez-/, '')
16
18
  end.sort
17
19
 
18
- commands.each do |cmd|
19
- puts " #{cmd}"
20
+ max_len = subcommands.map(&:length).max || 0
21
+
22
+ subcommands.each do |cmd|
23
+ cmd_path = bin_dir.join("fez-#{cmd}")
24
+ info = `#{cmd_path} --info 2>/dev/null`.strip
25
+ if info.empty?
26
+ puts " #{cmd}"
27
+ else
28
+ puts " #{cmd.ljust(max_len)} #{info}"
29
+ end
20
30
  end
21
31
 
22
32
  exit 0
23
33
  end
24
34
 
25
- subcommand_path = File.join(File.dirname(__FILE__), "fez-#{command}")
35
+ subcommand_path = bin_dir.join("fez-#{command}")
26
36
 
27
37
  if File.exist?(subcommand_path) && File.executable?(subcommand_path)
28
- exec(subcommand_path, *args)
38
+ exec(subcommand_path.to_s, *args)
29
39
  else
30
40
  puts "fez: '#{command}' is not a fez command. See 'fez --help'."
31
41
  exit 1
@@ -0,0 +1,347 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { parseArgs } from 'util'
4
+ import fs from 'fs'
5
+ import path from 'path'
6
+ import prettier from 'prettier'
7
+
8
+ const INFO = 'Compile Fez components and report errors'
9
+
10
+ const { values, positionals } = parseArgs({
11
+ args: Bun.argv.slice(2),
12
+ options: {
13
+ help: { type: 'boolean', short: 'h' },
14
+ output: { type: 'boolean', short: 'o' },
15
+ info: { type: 'boolean' },
16
+ },
17
+ allowPositionals: true,
18
+ })
19
+
20
+ if (values.info) {
21
+ console.log(INFO)
22
+ process.exit(0)
23
+ }
24
+
25
+ if (values.help || positionals.length === 0) {
26
+ console.log(`Usage: fez compile <file.fez>
27
+
28
+ ${INFO}
29
+
30
+ Options:
31
+ -h, --help Show this help message
32
+ -o, --output Output compiled JavaScript
33
+
34
+ Examples:
35
+ fez compile demo/fez/ui-counter.fez
36
+ fez compile -o demo/fez/ui-counter.fez > compiled.js
37
+ `)
38
+ process.exit(0)
39
+ }
40
+
41
+ if (positionals.length > 1) {
42
+ console.error('Error: Only one file can be compiled at a time')
43
+ process.exit(1)
44
+ }
45
+
46
+ // Parse a .fez file into its components
47
+ function parseFezFile(content) {
48
+ const result = { script: '', style: '', html: '', head: '', demo: '', info: '' }
49
+ const lines = content.split('\n')
50
+
51
+ let currentBlock = []
52
+ let currentType = ''
53
+
54
+ for (let line of lines) {
55
+ const trimmedLine = line.trim()
56
+
57
+ // Start blocks - demo/info can contain other tags, so skip nested detection
58
+ if (trimmedLine.startsWith('<demo') && !result.demo && !currentType) {
59
+ currentType = 'demo'
60
+ } else if (trimmedLine.startsWith('<info') && !result.info && !currentType) {
61
+ currentType = 'info'
62
+ } else if (trimmedLine.startsWith('<script') && !result.script && currentType !== 'head' && currentType !== 'demo' && currentType !== 'info') {
63
+ currentType = 'script'
64
+ } else if (trimmedLine.startsWith('<head') && !result.script && currentType !== 'demo' && currentType !== 'info') {
65
+ currentType = 'head'
66
+ } else if (trimmedLine.startsWith('<style') && currentType !== 'demo' && currentType !== 'info') {
67
+ currentType = 'style'
68
+ } else if (trimmedLine.endsWith('</demo>') && currentType === 'demo') {
69
+ result.demo = currentBlock.join('\n')
70
+ currentBlock = []
71
+ currentType = ''
72
+ } else if (trimmedLine.endsWith('</info>') && currentType === 'info') {
73
+ result.info = currentBlock.join('\n')
74
+ currentBlock = []
75
+ currentType = ''
76
+ } else if (trimmedLine.endsWith('</script>') && currentType === 'script' && !result.script) {
77
+ result.script = currentBlock.join('\n')
78
+ currentBlock = []
79
+ currentType = ''
80
+ } else if (trimmedLine.endsWith('</style>') && currentType === 'style') {
81
+ result.style = currentBlock.join('\n')
82
+ currentBlock = []
83
+ currentType = ''
84
+ } else if ((trimmedLine.endsWith('</head>') || trimmedLine.endsWith('</header>')) && currentType === 'head') {
85
+ result.head = currentBlock.join('\n')
86
+ currentBlock = []
87
+ currentType = ''
88
+ } else if (currentType) {
89
+ currentBlock.push(currentType === 'demo' || currentType === 'info' ? line : trimmedLine)
90
+ } else {
91
+ result.html += line + '\n'
92
+ }
93
+ }
94
+
95
+ return result
96
+ }
97
+
98
+ // Wrap script content in a class if not already wrapped
99
+ function wrapInClass(script) {
100
+ if (/class\s+\{/.test(script)) {
101
+ return script
102
+ }
103
+ return `class {\n${script}\n}`
104
+ }
105
+
106
+ // Validate JavaScript syntax
107
+ function validateScript(script, filePath) {
108
+ const errors = []
109
+
110
+ if (!script.trim()) {
111
+ return errors
112
+ }
113
+
114
+ // Check for ES module imports
115
+ const hasImports = /^\s*import\s+/m.test(script)
116
+
117
+ // Check if script has explicit class { } declaration
118
+ const hasExplicitClass = /class\s+\{/.test(script)
119
+
120
+ if (hasExplicitClass) {
121
+ // Split into parts: imports/top-level code and class
122
+ let parts = script.split(/class\s+\{/, 2)
123
+ let preamble = parts[0] || ''
124
+ let classBody = parts[1] ? `class {\n${parts[1]}` : ''
125
+
126
+ // Validate preamble (imports, const declarations)
127
+ if (preamble.trim()) {
128
+ // Remove import statements for validation (they're valid but can't be validated with new Function)
129
+ let preambleWithoutImports = preamble.replace(/^\s*import\s+.*$/gm, '// import removed')
130
+
131
+ if (preambleWithoutImports.trim()) {
132
+ try {
133
+ new Function(preambleWithoutImports)
134
+ } catch (e) {
135
+ if (!hasImports || !e.message.includes('import')) {
136
+ const lineMatch = e.stack?.match(/<anonymous>:(\d+):(\d+)/)
137
+ let errorInfo = { message: e.message, file: filePath, kind: 'JavaScript' }
138
+ if (lineMatch) {
139
+ errorInfo.line = parseInt(lineMatch[1])
140
+ errorInfo.column = parseInt(lineMatch[2])
141
+ }
142
+ errors.push(errorInfo)
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ // Validate class body
149
+ if (classBody.trim()) {
150
+ const code = `(${classBody})`
151
+ try {
152
+ new Function(code)
153
+ } catch (e) {
154
+ const lineMatch = e.stack?.match(/<anonymous>:(\d+):(\d+)/)
155
+ let errorInfo = { message: e.message, file: filePath, kind: 'JavaScript' }
156
+ if (lineMatch) {
157
+ // Adjust for preamble lines
158
+ const preambleLines = preamble.split('\n').length
159
+ errorInfo.line = parseInt(lineMatch[1]) + preambleLines - 2
160
+ errorInfo.column = parseInt(lineMatch[2])
161
+ }
162
+ errors.push(errorInfo)
163
+ }
164
+ }
165
+ } else {
166
+ // No explicit class - wrap entire script in class
167
+ let klass = wrapInClass(script)
168
+ const code = `(${klass})`
169
+ try {
170
+ new Function(code)
171
+ } catch (e) {
172
+ const lineMatch = e.stack?.match(/<anonymous>:(\d+):(\d+)/)
173
+ let errorInfo = { message: e.message, file: filePath, kind: 'JavaScript' }
174
+ if (lineMatch) {
175
+ errorInfo.line = parseInt(lineMatch[1]) - 2
176
+ errorInfo.column = parseInt(lineMatch[2])
177
+ }
178
+ errors.push(errorInfo)
179
+ }
180
+ }
181
+
182
+ return errors
183
+ }
184
+
185
+ // Validate template syntax (basic checks)
186
+ function validateTemplate(html, filePath) {
187
+ const errors = []
188
+
189
+ // Count both {{if and {{#if variants
190
+ const ifOpens = (html.match(/\{\{#?if\s/g) || []).length
191
+ const ifCloses = (html.match(/\{\{\/?#?if\}\}/g) || []).length
192
+ if (ifOpens !== ifCloses) {
193
+ errors.push({
194
+ message: `Unmatched {{if}} blocks: ${ifOpens} opens, ${ifCloses} closes`,
195
+ file: filePath,
196
+ kind: 'Template'
197
+ })
198
+ }
199
+
200
+ // Check for unmatched {{for}}/{{/for}}
201
+ const forOpens = (html.match(/\{\{for\s/g) || []).length
202
+ const forCloses = (html.match(/\{\{\/for\}\}/g) || []).length
203
+ if (forOpens !== forCloses) {
204
+ errors.push({
205
+ message: `Unmatched {{for}} blocks: ${forOpens} opens, ${forCloses} closes`,
206
+ file: filePath,
207
+ kind: 'Template'
208
+ })
209
+ }
210
+
211
+ // Check for {{if}} inside attributes (common mistake)
212
+ const attrIfMatch = html.match(/\w+=["'][^"']*\{\{#?if\s/g)
213
+ if (attrIfMatch) {
214
+ errors.push({
215
+ message: `{{if}} block found inside attribute - use ternary operator instead`,
216
+ file: filePath,
217
+ kind: 'Template'
218
+ })
219
+ }
220
+
221
+ return errors
222
+ }
223
+
224
+ // Generate compiled JavaScript from parsed .fez file
225
+ function generateCompiledJS(parsed, componentName) {
226
+ let klass = parsed.script
227
+
228
+ if (!/class\s+\{/.test(klass)) {
229
+ klass = `class {\n${klass}\n}`
230
+ }
231
+
232
+ // Add CSS if present
233
+ if (parsed.style && parsed.style.includes(':')) {
234
+ let style = parsed.style
235
+ // Wrap in :fez if not already scoped
236
+ if (!style.includes(':fez') && !/(?:^|\s)body\s*\{/.test(style)) {
237
+ style = `:fez {\n${style}\n}`
238
+ }
239
+ klass = klass.replace(/\}\s*$/, `\n CSS = \`${style}\`\n}`)
240
+ }
241
+
242
+ // Add HTML if present
243
+ if (parsed.html && /\w/.test(parsed.html)) {
244
+ // Escape backticks and dollar signs in template, trim whitespace
245
+ let html = parsed.html
246
+ .trim()
247
+ .replaceAll('`', '&#x60;')
248
+ .replaceAll('$', '\\$')
249
+ klass = klass.replace(/\}\s*$/, `\n HTML = \`${html}\`\n}`)
250
+ }
251
+
252
+ // Split into preamble (imports) and class body
253
+ let parts = klass.split(/class\s+\{/, 2)
254
+ let preamble = parts[0] || ''
255
+ let classBody = parts[1] ? `class {\n${parts[1]}` : klass
256
+
257
+ // Build final output
258
+ let output = ''
259
+ if (preamble.trim()) {
260
+ output += preamble.trim() + '\n\n'
261
+ }
262
+ output += `Fez('${componentName}', ${classBody})`
263
+
264
+ return output
265
+ }
266
+
267
+ async function formatCompiledJS(code, filePath) {
268
+ try {
269
+ return { code: await prettier.format(code, { parser: 'babel' }) }
270
+ } catch (error) {
271
+ return {
272
+ error: {
273
+ message: error?.message || 'Failed to format compiled JavaScript',
274
+ file: filePath,
275
+ kind: 'Format',
276
+ },
277
+ }
278
+ }
279
+ }
280
+
281
+ // Process a single file
282
+ async function compileFile(filePath) {
283
+ const errors = []
284
+ const absolutePath = path.resolve(filePath)
285
+
286
+ if (!fs.existsSync(absolutePath)) {
287
+ return {
288
+ errors: [{ message: `File not found: ${filePath}`, file: filePath, kind: 'File' }],
289
+ compiled: null,
290
+ }
291
+ }
292
+
293
+ const content = fs.readFileSync(absolutePath, 'utf-8')
294
+ const componentName = path.basename(filePath, '.fez')
295
+
296
+ // Validate component name has a dash
297
+ if (!componentName.includes('-')) {
298
+ errors.push({
299
+ message: `Invalid component name "${componentName}". Custom element names must contain a dash (e.g., 'my-element', 'ui-button').`,
300
+ file: filePath,
301
+ kind: 'Naming'
302
+ })
303
+ }
304
+
305
+ const parsed = parseFezFile(content)
306
+
307
+ // Validate script
308
+ errors.push(...validateScript(parsed.script, filePath))
309
+
310
+ // Validate template
311
+ errors.push(...validateTemplate(parsed.html, filePath))
312
+
313
+ // Generate compiled output only if no errors
314
+ let compiled = errors.length === 0 ? generateCompiledJS(parsed, componentName) : null
315
+ if (compiled) {
316
+ const formatted = await formatCompiledJS(compiled, filePath)
317
+ if (formatted.error) {
318
+ errors.push(formatted.error)
319
+ compiled = null
320
+ } else {
321
+ compiled = formatted.code
322
+ }
323
+ }
324
+
325
+ return { errors, compiled }
326
+ }
327
+
328
+ // Main execution
329
+ const filePath = positionals[0]
330
+ const { errors, compiled } = await compileFile(filePath)
331
+
332
+ if (errors.length > 0) {
333
+ for (const error of errors) {
334
+ const location = error.line ? `:${error.line}${error.column ? ':' + error.column : ''}` : ''
335
+ const kind = error.kind ? `${error.kind} error: ` : ''
336
+ console.error(`${error.file}${location}: ${kind}${error.message}`)
337
+ }
338
+ process.exit(1)
339
+ }
340
+
341
+ if (values.output && compiled) {
342
+ console.log(compiled)
343
+ } else {
344
+ console.log('Compiled without errors, add -o to show output')
345
+ }
346
+
347
+ process.exit(0)
package/bin/fez-debug ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ if ARGV[0] == '--info'
4
+ puts "Debug URL with Playwright (for LLM agents)"
5
+ exit 0
6
+ end
7
+
8
+ url = ARGV[0]
9
+
10
+ if url.nil? || url == '--help' || url == '-h'
11
+ puts "Usage: fez debug <url>"
12
+ puts ""
13
+ puts "Opens a Playwright browser session for LLM agents to debug web pages."
14
+ puts "The page object is exposed globally for programmatic inspection."
15
+ puts ""
16
+ puts "Example:"
17
+ puts " fez debug http://localhost:3333"
18
+ exit 0
19
+ end
20
+
21
+ bin_dir = File.dirname(File.realpath(__FILE__))
22
+ project_dir = File.dirname(bin_dir)
23
+ script_path = File.join(project_dir, 'bun', 'playwright-debug.js')
24
+
25
+ exec('bun', script_path, url)
package/bin/fez-index CHANGED
@@ -3,10 +3,22 @@
3
3
  require 'pathname'
4
4
  require 'json'
5
5
 
6
- # Check if argument is provided
7
- if ARGV.empty?
8
- puts "Usage: #{$0} <directory>"
9
- exit 1
6
+ INFO = 'Generate JSON index of files in a directory'
7
+
8
+ if ARGV[0] == '--info'
9
+ puts INFO
10
+ exit 0
11
+ end
12
+
13
+ if ARGV.empty? || ARGV[0] == '-h' || ARGV[0] == '--help'
14
+ puts "Usage: fez index <directory>"
15
+ puts ""
16
+ puts INFO
17
+ puts ""
18
+ puts "Examples:"
19
+ puts " fez index demo/fez"
20
+ puts " fez index assets/*"
21
+ exit 0
10
22
  end
11
23
 
12
24
  dir = ARGV[0]