@progalaxyelabs/htms-cli 0.3.0
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/README.md +108 -0
- package/bin/htms.js +113 -0
- package/package.json +29 -0
- package/src/compiler.js +87 -0
- package/src/format-errors.js +73 -0
- package/src/index.js +8 -0
- package/src/vite-plugin.js +133 -0
- package/src/watch.js +60 -0
- package/test/error.htms +3 -0
- package/test/output/events.ts +108 -0
- package/test/output/router.ts +58 -0
- package/test/output/templates.ts +52 -0
- package/test/test.htms +18 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# HTMS CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for the HTMS compiler.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @htms/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Compile
|
|
14
|
+
|
|
15
|
+
Compile a `.htms` file to TypeScript:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
htms compile src/app.htms -o dist/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
- `-o, --output <dir>` - Output directory (default: `dist`)
|
|
23
|
+
- `-w, --watch` - Watch for changes
|
|
24
|
+
- `-q, --quiet` - Suppress output
|
|
25
|
+
|
|
26
|
+
### Check
|
|
27
|
+
|
|
28
|
+
Validate a `.htms` file without generating output:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
htms check src/app.htms
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Watch Mode
|
|
35
|
+
|
|
36
|
+
Watch for changes and recompile automatically:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
htms compile src/app.htms -o dist/ --watch
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Init
|
|
43
|
+
|
|
44
|
+
Initialize a new HTMS project (coming soon):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
htms init
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Programmatic API
|
|
51
|
+
|
|
52
|
+
You can also use the CLI programmatically:
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
import { compileFile, checkFile } from '@htms/cli';
|
|
56
|
+
|
|
57
|
+
const result = await compileFile('src/app.htms', 'dist/');
|
|
58
|
+
console.log(result.success); // true/false
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Vite Plugin
|
|
62
|
+
|
|
63
|
+
Use HTMS in your Vite project:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// vite.config.js
|
|
67
|
+
import { htmsPlugin } from '@htms/cli/vite';
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
plugins: [htmsPlugin({
|
|
71
|
+
include: /\.htms$/,
|
|
72
|
+
outputDir: 'src/generated',
|
|
73
|
+
watch: true
|
|
74
|
+
})]
|
|
75
|
+
};
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Plugin Options
|
|
79
|
+
|
|
80
|
+
- `include` - RegExp pattern for files to process (default: `/\.htms$/`)
|
|
81
|
+
- `outputDir` - Directory for generated files (default: `'src/generated'`)
|
|
82
|
+
- `watch` - Enable watch mode (default: `true`)
|
|
83
|
+
|
|
84
|
+
## Output Files
|
|
85
|
+
|
|
86
|
+
The compiler generates three TypeScript files:
|
|
87
|
+
|
|
88
|
+
1. **templates.ts** - Component/section/page functions
|
|
89
|
+
2. **router.ts** - Hash-based router with context management
|
|
90
|
+
3. **events.ts** - Event delegation and two-way binding
|
|
91
|
+
|
|
92
|
+
## Error Formatting
|
|
93
|
+
|
|
94
|
+
Errors are displayed with source context:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
error[E002]: Undefined component: 'NavBar'
|
|
98
|
+
--> src/app.htms:10:5
|
|
99
|
+
|
|
100
|
+
9 | page home "/" {
|
|
101
|
+
10 | NavBar
|
|
102
|
+
| ^^^^^^
|
|
103
|
+
11 | }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/bin/htms.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { compileFile, checkFile, getStats } from '../src/compiler.js';
|
|
5
|
+
import { watchFiles } from '../src/watch.js';
|
|
6
|
+
import { readFile } from 'fs/promises';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const packageJson = JSON.parse(
|
|
13
|
+
await readFile(join(__dirname, '../package.json'), 'utf-8')
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('htms')
|
|
20
|
+
.description('HTMS compiler - compile .htms files to TypeScript')
|
|
21
|
+
.version(packageJson.version);
|
|
22
|
+
|
|
23
|
+
// Compile command
|
|
24
|
+
program
|
|
25
|
+
.command('compile')
|
|
26
|
+
.description('Compile .htms file to TypeScript')
|
|
27
|
+
.argument('<input>', 'Input .htms file')
|
|
28
|
+
.option('-o, --output <dir>', 'Output directory', 'dist')
|
|
29
|
+
.option('-w, --watch', 'Watch for changes')
|
|
30
|
+
.option('-q, --quiet', 'Suppress output')
|
|
31
|
+
.action(async (input, options) => {
|
|
32
|
+
try {
|
|
33
|
+
if (options.watch) {
|
|
34
|
+
watchFiles(input, options.output, options);
|
|
35
|
+
} else {
|
|
36
|
+
const result = await compileFile(input, options.output, options);
|
|
37
|
+
const stats = getStats(result);
|
|
38
|
+
|
|
39
|
+
if (result.success) {
|
|
40
|
+
if (!options.quiet) {
|
|
41
|
+
console.log(pc.green('\n✓ Compilation successful!'));
|
|
42
|
+
console.log(pc.gray(` Generated ${stats.files} file(s)`));
|
|
43
|
+
if (stats.warnings > 0) {
|
|
44
|
+
console.log(pc.yellow(` ${stats.warnings} warning(s)`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
process.exit(0);
|
|
48
|
+
} else {
|
|
49
|
+
if (!options.quiet) {
|
|
50
|
+
console.error(pc.red('\n✗ Compilation failed!'));
|
|
51
|
+
console.error(pc.gray(` ${stats.errors} error(s)`));
|
|
52
|
+
}
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(pc.red('Error:'), error.message);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Check command
|
|
63
|
+
program
|
|
64
|
+
.command('check')
|
|
65
|
+
.description('Check .htms file for errors without generating output')
|
|
66
|
+
.argument('<input>', 'Input .htms file')
|
|
67
|
+
.action(async (input) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await checkFile(input);
|
|
70
|
+
const stats = getStats(result);
|
|
71
|
+
|
|
72
|
+
if (result.success) {
|
|
73
|
+
console.log(pc.green('\n✓ No errors found!'));
|
|
74
|
+
if (stats.warnings > 0) {
|
|
75
|
+
console.log(pc.yellow(` ${stats.warnings} warning(s)`));
|
|
76
|
+
}
|
|
77
|
+
process.exit(0);
|
|
78
|
+
} else {
|
|
79
|
+
console.error(pc.red('\n✗ Validation failed!'));
|
|
80
|
+
console.error(pc.gray(` ${stats.errors} error(s)`));
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(pc.red('Error:'), error.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Init command (scaffold new project)
|
|
90
|
+
program
|
|
91
|
+
.command('init')
|
|
92
|
+
.description('Initialize a new HTMS project')
|
|
93
|
+
.option('-d, --dir <directory>', 'Project directory', '.')
|
|
94
|
+
.action(async (options) => {
|
|
95
|
+
console.log(pc.blue('📦 Initializing new HTMS project...'));
|
|
96
|
+
console.log(pc.gray(` Directory: ${options.dir}\n`));
|
|
97
|
+
|
|
98
|
+
// This would create a basic project structure
|
|
99
|
+
// For now, just show what would be created
|
|
100
|
+
console.log(pc.gray('Would create:'));
|
|
101
|
+
console.log(pc.gray(' src/'));
|
|
102
|
+
console.log(pc.gray(' app.htms'));
|
|
103
|
+
console.log(pc.gray(' actions.ts'));
|
|
104
|
+
console.log(pc.gray(' runtime.ts'));
|
|
105
|
+
console.log(pc.gray(' dist/'));
|
|
106
|
+
console.log(pc.gray(' package.json'));
|
|
107
|
+
console.log(pc.gray(' vite.config.ts\n'));
|
|
108
|
+
|
|
109
|
+
console.log(pc.yellow('⚠ Init command not yet implemented'));
|
|
110
|
+
console.log(pc.gray(' This is a placeholder for future functionality'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@progalaxyelabs/htms-cli",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "CLI for HTMS compiler",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"htms": "./bin/htms.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/index.js",
|
|
12
|
+
"./vite": "./src/vite-plugin.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["htms", "compiler", "cli"],
|
|
18
|
+
"author": "ProGalaxy Labs",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@progalaxyelabs/htms-compiler": "^0.3.0",
|
|
22
|
+
"commander": "^11.0.0",
|
|
23
|
+
"chokidar": "^3.5.0",
|
|
24
|
+
"picocolors": "^1.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/compiler.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { compile_wasm, init } from '@progalaxyelabs/htms-compiler';
|
|
2
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { dirname, join, basename } from 'path';
|
|
4
|
+
import { printDiagnostics } from './format-errors.js';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
|
|
7
|
+
// Initialize WASM module
|
|
8
|
+
let wasmInitialized = false;
|
|
9
|
+
async function ensureWasmInit() {
|
|
10
|
+
if (!wasmInitialized) {
|
|
11
|
+
await init();
|
|
12
|
+
wasmInitialized = true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compile HTMS file to TypeScript
|
|
18
|
+
* @param {string} inputPath - Path to .htms file
|
|
19
|
+
* @param {string} outputDir - Output directory for generated files
|
|
20
|
+
* @param {object} options - Compilation options
|
|
21
|
+
* @returns {Promise<{success: boolean, diagnostics: Array}>}
|
|
22
|
+
*/
|
|
23
|
+
export async function compileFile(inputPath, outputDir, options = {}) {
|
|
24
|
+
await ensureWasmInit();
|
|
25
|
+
|
|
26
|
+
// Read source file
|
|
27
|
+
const source = await readFile(inputPath, 'utf-8');
|
|
28
|
+
|
|
29
|
+
// Compile using WASM
|
|
30
|
+
const result = compile_wasm(source);
|
|
31
|
+
|
|
32
|
+
// Print diagnostics
|
|
33
|
+
if (result.diagnostics && result.diagnostics.length > 0) {
|
|
34
|
+
printDiagnostics(result.diagnostics, source, inputPath);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Write output files if successful
|
|
38
|
+
if (result.success && result.files) {
|
|
39
|
+
await mkdir(outputDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
for (const file of result.files) {
|
|
42
|
+
const outputPath = join(outputDir, file.path);
|
|
43
|
+
await writeFile(outputPath, file.content, 'utf-8');
|
|
44
|
+
|
|
45
|
+
if (!options.quiet) {
|
|
46
|
+
console.log(pc.green('✓') + ` Generated ${pc.cyan(file.path)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check HTMS file for errors without generating output
|
|
56
|
+
* @param {string} inputPath - Path to .htms file
|
|
57
|
+
* @returns {Promise<{success: boolean, diagnostics: Array}>}
|
|
58
|
+
*/
|
|
59
|
+
export async function checkFile(inputPath) {
|
|
60
|
+
await ensureWasmInit();
|
|
61
|
+
|
|
62
|
+
// Read source file
|
|
63
|
+
const source = await readFile(inputPath, 'utf-8');
|
|
64
|
+
|
|
65
|
+
// Compile (but don't write output)
|
|
66
|
+
const result = compile_wasm(source);
|
|
67
|
+
|
|
68
|
+
// Print diagnostics
|
|
69
|
+
if (result.diagnostics && result.diagnostics.length > 0) {
|
|
70
|
+
printDiagnostics(result.diagnostics, source, inputPath);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get compilation statistics
|
|
78
|
+
* @param {object} result - Compilation result
|
|
79
|
+
* @returns {object} Statistics
|
|
80
|
+
*/
|
|
81
|
+
export function getStats(result) {
|
|
82
|
+
const errors = result.diagnostics?.filter(d => d.severity === 'Error').length || 0;
|
|
83
|
+
const warnings = result.diagnostics?.filter(d => d.severity === 'Warning').length || 0;
|
|
84
|
+
const files = result.files?.length || 0;
|
|
85
|
+
|
|
86
|
+
return { errors, warnings, files };
|
|
87
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format diagnostics with source context
|
|
5
|
+
* @param {Array} diagnostics - Array of diagnostic objects
|
|
6
|
+
* @param {string} source - Original source code
|
|
7
|
+
* @param {string} filename - Filename for display
|
|
8
|
+
* @returns {string} Formatted error messages
|
|
9
|
+
*/
|
|
10
|
+
export function formatDiagnostics(diagnostics, source, filename = 'input.htms') {
|
|
11
|
+
if (diagnostics.length === 0) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const lines = source.split('\n');
|
|
16
|
+
const output = [];
|
|
17
|
+
|
|
18
|
+
for (const diag of diagnostics) {
|
|
19
|
+
const { severity, message, location, code } = diag;
|
|
20
|
+
const { line, column } = location;
|
|
21
|
+
|
|
22
|
+
// Header with severity
|
|
23
|
+
const severityStr = severity === 'Error'
|
|
24
|
+
? pc.red(pc.bold('error'))
|
|
25
|
+
: pc.yellow(pc.bold('warning'));
|
|
26
|
+
|
|
27
|
+
const codeStr = code ? pc.gray(`[${code}]`) : '';
|
|
28
|
+
output.push(`${severityStr}${codeStr}: ${message}`);
|
|
29
|
+
|
|
30
|
+
// Location
|
|
31
|
+
output.push(pc.cyan(` --> ${filename}:${line}:${column}`));
|
|
32
|
+
output.push('');
|
|
33
|
+
|
|
34
|
+
// Source context (line before, error line, line after)
|
|
35
|
+
const startLine = Math.max(1, line - 1);
|
|
36
|
+
const endLine = Math.min(lines.length, line + 1);
|
|
37
|
+
|
|
38
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
39
|
+
const lineNum = String(i).padStart(4, ' ');
|
|
40
|
+
const sourceLine = lines[i - 1] || '';
|
|
41
|
+
|
|
42
|
+
if (i === line) {
|
|
43
|
+
// Error line
|
|
44
|
+
output.push(pc.gray(`${lineNum} | `) + sourceLine);
|
|
45
|
+
|
|
46
|
+
// Underline the error
|
|
47
|
+
const padding = ' '.repeat(column - 1);
|
|
48
|
+
const underline = pc.red('^'.repeat(Math.max(1, sourceLine.length - column + 1)));
|
|
49
|
+
output.push(pc.gray(' | ') + padding + underline);
|
|
50
|
+
} else {
|
|
51
|
+
// Context line
|
|
52
|
+
output.push(pc.gray(`${lineNum} | ${sourceLine}`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
output.push('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return output.join('\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Print diagnostics to console
|
|
64
|
+
* @param {Array} diagnostics - Array of diagnostic objects
|
|
65
|
+
* @param {string} source - Original source code
|
|
66
|
+
* @param {string} filename - Filename for display
|
|
67
|
+
*/
|
|
68
|
+
export function printDiagnostics(diagnostics, source, filename) {
|
|
69
|
+
const formatted = formatDiagnostics(diagnostics, source, filename);
|
|
70
|
+
if (formatted) {
|
|
71
|
+
console.error(formatted);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTMS CLI - Main exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { compileFile, checkFile, getStats } from './compiler.js';
|
|
6
|
+
export { watchFiles } from './watch.js';
|
|
7
|
+
export { formatDiagnostics, printDiagnostics } from './format-errors.js';
|
|
8
|
+
export { htmsPlugin } from './vite-plugin.js';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { compile_wasm, init } from '@htms/compiler';
|
|
2
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
3
|
+
import { dirname, join, relative } from 'path';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
|
|
6
|
+
let wasmInitialized = false;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Vite plugin for HTMS compilation
|
|
10
|
+
* @param {object} options - Plugin options
|
|
11
|
+
* @returns {import('vite').Plugin}
|
|
12
|
+
*/
|
|
13
|
+
export function htmsPlugin(options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
include = /\.htms$/,
|
|
16
|
+
outputDir = 'src/generated',
|
|
17
|
+
watch = true
|
|
18
|
+
} = options;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
name: 'vite-plugin-htms',
|
|
22
|
+
|
|
23
|
+
async buildStart() {
|
|
24
|
+
if (!wasmInitialized) {
|
|
25
|
+
await init();
|
|
26
|
+
wasmInitialized = true;
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
async handleHotUpdate({ file, server }) {
|
|
31
|
+
// Only process .htms files
|
|
32
|
+
if (!include.test(file)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(pc.blue('[htms]') + ` Compiling ${pc.cyan(relative(process.cwd(), file))}`);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const source = await readFile(file, 'utf-8');
|
|
40
|
+
const result = compile_wasm(source);
|
|
41
|
+
|
|
42
|
+
if (result.success && result.files) {
|
|
43
|
+
// Write generated files
|
|
44
|
+
for (const genFile of result.files) {
|
|
45
|
+
const outputPath = join(outputDir, genFile.path);
|
|
46
|
+
await writeFile(outputPath, genFile.content, 'utf-8');
|
|
47
|
+
console.log(pc.green(' ✓') + ` Generated ${pc.gray(genFile.path)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Trigger HMR for generated files
|
|
51
|
+
const modules = [];
|
|
52
|
+
for (const genFile of result.files) {
|
|
53
|
+
const modulePath = join(outputDir, genFile.path);
|
|
54
|
+
const module = server.moduleGraph.getModuleById(modulePath);
|
|
55
|
+
if (module) {
|
|
56
|
+
modules.push(module);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (modules.length > 0) {
|
|
61
|
+
return modules;
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
// Print errors
|
|
65
|
+
console.error(pc.red(' ✗') + ` Compilation failed`);
|
|
66
|
+
for (const diag of result.diagnostics || []) {
|
|
67
|
+
if (diag.severity === 'Error') {
|
|
68
|
+
console.error(pc.red(` ${diag.message}`) + pc.gray(` at line ${diag.location.line}`));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(pc.red('[htms]') + ` Error: ${error.message}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return [];
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async transform(code, id) {
|
|
80
|
+
// Only process .htms files
|
|
81
|
+
if (!include.test(id)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = compile_wasm(code);
|
|
87
|
+
|
|
88
|
+
if (!result.success) {
|
|
89
|
+
// Show errors in console
|
|
90
|
+
console.error(pc.red('[htms]') + ` Compilation failed for ${id}`);
|
|
91
|
+
for (const diag of result.diagnostics || []) {
|
|
92
|
+
if (diag.severity === 'Error') {
|
|
93
|
+
console.error(pc.red(` ${diag.message}`) + pc.gray(` at line ${diag.location.line}`));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Return error as module
|
|
98
|
+
const errorMsg = result.diagnostics
|
|
99
|
+
?.filter(d => d.severity === 'Error')
|
|
100
|
+
.map(d => `${d.message} (line ${d.location.line})`)
|
|
101
|
+
.join('\\n');
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
code: `throw new Error('HTMS compilation failed:\\n${errorMsg}');`,
|
|
105
|
+
map: null
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Write generated files to output directory
|
|
110
|
+
if (result.files) {
|
|
111
|
+
for (const file of result.files) {
|
|
112
|
+
const outputPath = join(outputDir, file.path);
|
|
113
|
+
await writeFile(outputPath, file.content, 'utf-8');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Return a module that imports the generated templates
|
|
118
|
+
return {
|
|
119
|
+
code: `export { default } from '${outputDir}/templates.js';`,
|
|
120
|
+
map: null
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(pc.red('[htms]') + ` Error processing ${id}:`, error.message);
|
|
124
|
+
return {
|
|
125
|
+
code: `throw new Error('HTMS plugin error: ${error.message}');`,
|
|
126
|
+
map: null
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default htmsPlugin;
|
package/src/watch.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import { compileFile, getStats } from './compiler.js';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Watch HTMS files for changes and recompile
|
|
7
|
+
* @param {string} pattern - Glob pattern for files to watch
|
|
8
|
+
* @param {string} outputDir - Output directory
|
|
9
|
+
* @param {object} options - Watch options
|
|
10
|
+
*/
|
|
11
|
+
export function watchFiles(pattern, outputDir, options = {}) {
|
|
12
|
+
console.log(pc.blue('👁 Watching for changes...'));
|
|
13
|
+
console.log(pc.gray(` Pattern: ${pattern}`));
|
|
14
|
+
console.log(pc.gray(` Output: ${outputDir}\n`));
|
|
15
|
+
|
|
16
|
+
const watcher = chokidar.watch(pattern, {
|
|
17
|
+
persistent: true,
|
|
18
|
+
ignoreInitial: false,
|
|
19
|
+
awaitWriteFinish: {
|
|
20
|
+
stabilityThreshold: 100,
|
|
21
|
+
pollInterval: 50
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
watcher
|
|
26
|
+
.on('add', path => handleChange(path, outputDir, 'added', options))
|
|
27
|
+
.on('change', path => handleChange(path, outputDir, 'changed', options))
|
|
28
|
+
.on('error', error => console.error(pc.red(`Watcher error: ${error}`)));
|
|
29
|
+
|
|
30
|
+
// Handle graceful shutdown
|
|
31
|
+
process.on('SIGINT', () => {
|
|
32
|
+
console.log('\n' + pc.blue('Stopping watcher...'));
|
|
33
|
+
watcher.close();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return watcher;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function handleChange(filePath, outputDir, action, options) {
|
|
41
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
42
|
+
console.log(pc.gray(`[${timestamp}]`) + ` File ${action}: ${pc.cyan(filePath)}`);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await compileFile(filePath, outputDir, { ...options, quiet: false });
|
|
46
|
+
const stats = getStats(result);
|
|
47
|
+
|
|
48
|
+
if (result.success) {
|
|
49
|
+
console.log(pc.green('✓') + ` Compiled successfully (${stats.files} files generated)`);
|
|
50
|
+
if (stats.warnings > 0) {
|
|
51
|
+
console.log(pc.yellow('⚠') + ` ${stats.warnings} warning(s)`);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
console.log(pc.red('✗') + ` Compilation failed (${stats.errors} error(s))`);
|
|
55
|
+
}
|
|
56
|
+
console.log('');
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(pc.red('Error:') + ` ${error.message}\n`);
|
|
59
|
+
}
|
|
60
|
+
}
|
package/test/error.htms
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Generated by HTMS Compiler
|
|
2
|
+
// Do not edit manually
|
|
3
|
+
|
|
4
|
+
import { getContext, setContext, rerender } from './router';
|
|
5
|
+
import { actions } from './actions';
|
|
6
|
+
|
|
7
|
+
export interface ActionContext {
|
|
8
|
+
data: Record<string, unknown>;
|
|
9
|
+
rerender: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function initEvents(): void {
|
|
13
|
+
// Event delegation
|
|
14
|
+
document.addEventListener('click', handleEvent);
|
|
15
|
+
document.addEventListener('submit', handleEvent);
|
|
16
|
+
document.addEventListener('input', handleEvent);
|
|
17
|
+
document.addEventListener('change', handleEvent);
|
|
18
|
+
document.addEventListener('blur', handleEvent, true);
|
|
19
|
+
document.addEventListener('focus', handleEvent, true);
|
|
20
|
+
|
|
21
|
+
// Two-way binding
|
|
22
|
+
initBinding();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleEvent(event: Event): void {
|
|
26
|
+
const target = event.target as HTMLElement;
|
|
27
|
+
const actionEl = target.closest('[data-action]') as HTMLElement;
|
|
28
|
+
|
|
29
|
+
if (!actionEl) return;
|
|
30
|
+
|
|
31
|
+
const actionName = actionEl.dataset.action;
|
|
32
|
+
const eventType = actionEl.dataset.event;
|
|
33
|
+
|
|
34
|
+
// Only handle if event type matches
|
|
35
|
+
if (eventType && eventType !== event.type) return;
|
|
36
|
+
|
|
37
|
+
// Handle modifiers
|
|
38
|
+
if (actionEl.dataset.prevent === 'true') {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
}
|
|
41
|
+
if (actionEl.dataset.stop === 'true') {
|
|
42
|
+
event.stopPropagation();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Get action
|
|
46
|
+
const action = (actions as Record<string, Function>)[actionName!];
|
|
47
|
+
if (action) {
|
|
48
|
+
const ctx: ActionContext = {
|
|
49
|
+
data: getContext(),
|
|
50
|
+
rerender: () => {
|
|
51
|
+
setContext(ctx.data);
|
|
52
|
+
rerender();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Parse arguments if present
|
|
57
|
+
const argsStr = actionEl.dataset.args;
|
|
58
|
+
if (argsStr) {
|
|
59
|
+
try {
|
|
60
|
+
const args = JSON.parse(argsStr);
|
|
61
|
+
action(...args)(ctx, event);
|
|
62
|
+
} catch {
|
|
63
|
+
action(ctx, event);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
action(ctx, event);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Handle once modifier
|
|
70
|
+
if (actionEl.dataset.once === 'true') {
|
|
71
|
+
actionEl.removeAttribute('data-action');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function initBinding(): void {
|
|
77
|
+
document.addEventListener('input', (e) => {
|
|
78
|
+
const el = e.target as HTMLInputElement;
|
|
79
|
+
const bindPath = el.dataset.bind;
|
|
80
|
+
if (bindPath) {
|
|
81
|
+
const ctx = getContext();
|
|
82
|
+
setNestedValue(ctx, bindPath, el.value);
|
|
83
|
+
setContext(ctx);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
document.addEventListener('change', (e) => {
|
|
88
|
+
const el = e.target as HTMLInputElement;
|
|
89
|
+
const bindPath = el.dataset.bind;
|
|
90
|
+
if (bindPath && el.type === 'checkbox') {
|
|
91
|
+
const ctx = getContext();
|
|
92
|
+
setNestedValue(ctx, bindPath, el.checked);
|
|
93
|
+
setContext(ctx);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
|
|
99
|
+
const keys = path.split('.');
|
|
100
|
+
const last = keys.pop()!;
|
|
101
|
+
const target = keys.reduce((o, k) => {
|
|
102
|
+
if (!(o as Record<string, unknown>)[k]) {
|
|
103
|
+
(o as Record<string, unknown>)[k] = {};
|
|
104
|
+
}
|
|
105
|
+
return (o as Record<string, unknown>)[k];
|
|
106
|
+
}, obj) as Record<string, unknown>;
|
|
107
|
+
target[last] = value;
|
|
108
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Generated by HTMS Compiler
|
|
2
|
+
// Do not edit manually
|
|
3
|
+
|
|
4
|
+
import { Router } from './runtime';
|
|
5
|
+
import { HomePage, AboutPage } from './templates';
|
|
6
|
+
|
|
7
|
+
// Application context
|
|
8
|
+
let context: Record<string, unknown> = {};
|
|
9
|
+
let currentPage: string = '';
|
|
10
|
+
const appContainer = document.getElementById('app');
|
|
11
|
+
|
|
12
|
+
export function getContext(): Record<string, unknown> {
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function setContext(data: Record<string, unknown>): void {
|
|
17
|
+
context = data;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rerender(): void {
|
|
21
|
+
if (currentPage && appContainer) {
|
|
22
|
+
const renderer = routes[currentPage];
|
|
23
|
+
if (renderer) {
|
|
24
|
+
appContainer.innerHTML = '';
|
|
25
|
+
appContainer.appendChild(renderer(context));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Route definitions
|
|
31
|
+
const routes: Record<string, (ctx: Record<string, unknown>) => HTMLElement> = {
|
|
32
|
+
'/': HomePage,
|
|
33
|
+
'/about': AboutPage,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function renderPage(route: string): void {
|
|
37
|
+
currentPage = route;
|
|
38
|
+
const renderer = routes[route];
|
|
39
|
+
if (renderer && appContainer) {
|
|
40
|
+
appContainer.innerHTML = '';
|
|
41
|
+
appContainer.appendChild(renderer(context));
|
|
42
|
+
} else if (appContainer) {
|
|
43
|
+
const el = document.createElement('h1');
|
|
44
|
+
el.textContent = '404 - Page Not Found';
|
|
45
|
+
appContainer.innerHTML = '';
|
|
46
|
+
appContainer.appendChild(el);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create router
|
|
51
|
+
export const router = new Router({
|
|
52
|
+
mode: 'hash',
|
|
53
|
+
routes: {
|
|
54
|
+
'/': () => renderPage('/'),
|
|
55
|
+
'/about': () => renderPage('/about'),
|
|
56
|
+
},
|
|
57
|
+
notFound: () => renderPage('__404__'),
|
|
58
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Generated by HTMS Compiler
|
|
2
|
+
// Do not edit manually
|
|
3
|
+
|
|
4
|
+
import { actions } from './actions';
|
|
5
|
+
|
|
6
|
+
export type Context = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export function NavBar(ctx: Context): HTMLElement {
|
|
9
|
+
const el0 = document.createElement('nav');
|
|
10
|
+
el0.className = 'navbar';
|
|
11
|
+
const el1 = document.createElement('a');
|
|
12
|
+
el1.href = '#/';
|
|
13
|
+
const el2 = document.createTextNode('Home');
|
|
14
|
+
el1.appendChild(el2);
|
|
15
|
+
el0.appendChild(el1);
|
|
16
|
+
const el3 = document.createElement('a');
|
|
17
|
+
el3.href = '#/about';
|
|
18
|
+
const el4 = document.createTextNode('About');
|
|
19
|
+
el3.appendChild(el4);
|
|
20
|
+
el0.appendChild(el3);
|
|
21
|
+
return el0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function HomePage(ctx: Context): HTMLElement {
|
|
25
|
+
const fragment = document.createDocumentFragment();
|
|
26
|
+
const el0 = NavBar(ctx);
|
|
27
|
+
fragment.appendChild(el0);
|
|
28
|
+
const el1 = document.createElement('div');
|
|
29
|
+
el1.className = 'hero';
|
|
30
|
+
const el2 = document.createElement('h1');
|
|
31
|
+
const el3 = document.createTextNode('Welcome to HTMS!');
|
|
32
|
+
el2.appendChild(el3);
|
|
33
|
+
el1.appendChild(el2);
|
|
34
|
+
fragment.appendChild(el1);
|
|
35
|
+
const root = document.createElement('div');
|
|
36
|
+
root.appendChild(fragment);
|
|
37
|
+
return root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function AboutPage(ctx: Context): HTMLElement {
|
|
41
|
+
const fragment = document.createDocumentFragment();
|
|
42
|
+
const el0 = NavBar(ctx);
|
|
43
|
+
fragment.appendChild(el0);
|
|
44
|
+
const el1 = document.createElement('div');
|
|
45
|
+
const el2 = document.createTextNode('About Page');
|
|
46
|
+
el1.appendChild(el2);
|
|
47
|
+
fragment.appendChild(el1);
|
|
48
|
+
const root = document.createElement('div');
|
|
49
|
+
root.appendChild(fragment);
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
52
|
+
|
package/test/test.htms
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
component NavBar {
|
|
2
|
+
nav [class: "navbar"] {
|
|
3
|
+
a [href: "#/"] {{ Home }}
|
|
4
|
+
a [href: "#/about"] {{ About }}
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
page home "/" {
|
|
9
|
+
NavBar
|
|
10
|
+
div [class: "hero"] {
|
|
11
|
+
h1 {{ Welcome to HTMS! }}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
page about "/about" {
|
|
16
|
+
NavBar
|
|
17
|
+
div {{ About Page }}
|
|
18
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
"checkJs": false,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.js", "bin/**/*.js"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|