pulse-js-framework 1.4.4 → 1.4.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/README.md +74 -0
- package/cli/index.js +23 -20
- package/cli/logger.js +122 -0
- package/index.js +8 -2
- package/loader/vite-plugin.js +22 -6
- package/package.json +37 -6
- package/runtime/dom.js +55 -11
- package/runtime/hmr.js +169 -0
- package/runtime/index.js +2 -0
- package/runtime/logger.js +304 -0
- package/runtime/native.js +7 -4
- package/runtime/pulse.js +446 -62
- package/runtime/store.js +227 -19
- package/types/index.d.ts +20 -1
- package/types/logger.d.ts +122 -0
- package/types/pulse.d.ts +33 -0
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ A declarative DOM framework with CSS selector-based structure and reactive pulsa
|
|
|
13
13
|
- **No Build Required** - Works directly in the browser
|
|
14
14
|
- **Lightweight** - Minimal footprint, maximum performance
|
|
15
15
|
- **Router & Store** - Built-in SPA routing and state management
|
|
16
|
+
- **Hot Module Replacement** - Full HMR with state preservation
|
|
16
17
|
- **Mobile Apps** - Build native Android & iOS apps (zero dependencies)
|
|
17
18
|
- **TypeScript Support** - Full type definitions for IDE autocomplete
|
|
18
19
|
|
|
@@ -280,6 +281,57 @@ const store = createStore({
|
|
|
280
281
|
store.user.set({ name: 'John' });
|
|
281
282
|
```
|
|
282
283
|
|
|
284
|
+
### HMR (Hot Module Replacement)
|
|
285
|
+
|
|
286
|
+
Pulse supports full HMR with state preservation during development:
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
import { createHMRContext } from 'pulse-js-framework/runtime/hmr';
|
|
290
|
+
|
|
291
|
+
const hmr = createHMRContext(import.meta.url);
|
|
292
|
+
|
|
293
|
+
// State preserved across HMR updates
|
|
294
|
+
const count = hmr.preservePulse('count', 0);
|
|
295
|
+
const items = hmr.preservePulse('items', []);
|
|
296
|
+
|
|
297
|
+
// Effects tracked for automatic cleanup
|
|
298
|
+
hmr.setup(() => {
|
|
299
|
+
effect(() => {
|
|
300
|
+
document.title = `Count: ${count.get()}`;
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Accept HMR updates
|
|
305
|
+
hmr.accept();
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**HMR Features:**
|
|
309
|
+
- `preservePulse(key, value)` - Create pulses that survive module replacement
|
|
310
|
+
- `setup(callback)` - Execute with effect tracking for cleanup
|
|
311
|
+
- Automatic event listener cleanup (no accumulation)
|
|
312
|
+
- Works with Vite dev server
|
|
313
|
+
|
|
314
|
+
### Logger
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
import { createLogger, setLogLevel, LogLevel } from 'pulse-js-framework/runtime/logger';
|
|
318
|
+
|
|
319
|
+
// Create a namespaced logger
|
|
320
|
+
const log = createLogger('MyComponent');
|
|
321
|
+
|
|
322
|
+
log.info('Component initialized'); // [MyComponent] Component initialized
|
|
323
|
+
log.warn('Deprecated method used');
|
|
324
|
+
log.error('Failed to load', error);
|
|
325
|
+
log.debug('Detailed info'); // Only shown at DEBUG level
|
|
326
|
+
|
|
327
|
+
// Set global log level
|
|
328
|
+
setLogLevel(LogLevel.DEBUG); // SILENT, ERROR, WARN, INFO, DEBUG
|
|
329
|
+
|
|
330
|
+
// Child loggers for sub-namespaces
|
|
331
|
+
const childLog = log.child('Validation');
|
|
332
|
+
childLog.info('Validating'); // [MyComponent:Validation] Validating
|
|
333
|
+
```
|
|
334
|
+
|
|
283
335
|
## CLI Commands
|
|
284
336
|
|
|
285
337
|
```bash
|
|
@@ -399,6 +451,28 @@ onNativeReady(({ platform }) => {
|
|
|
399
451
|
|
|
400
452
|
**Available APIs:** Storage, Device Info, Network Status, Toast, Vibration, Clipboard, App Lifecycle
|
|
401
453
|
|
|
454
|
+
## VSCode Extension
|
|
455
|
+
|
|
456
|
+
Pulse includes a VSCode extension for `.pulse` files with syntax highlighting and snippets.
|
|
457
|
+
|
|
458
|
+
### Installation
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# Windows (PowerShell)
|
|
462
|
+
cd vscode-extension
|
|
463
|
+
powershell -ExecutionPolicy Bypass -File install.ps1
|
|
464
|
+
|
|
465
|
+
# macOS/Linux
|
|
466
|
+
cd vscode-extension
|
|
467
|
+
bash install.sh
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Then restart VSCode. You'll get:
|
|
471
|
+
- Syntax highlighting for `.pulse` files
|
|
472
|
+
- Code snippets (`page`, `state`, `view`, `@click`, etc.)
|
|
473
|
+
- Bracket matching and auto-closing
|
|
474
|
+
- Comment toggling (Ctrl+/)
|
|
475
|
+
|
|
402
476
|
## TypeScript Support
|
|
403
477
|
|
|
404
478
|
Pulse includes full TypeScript definitions for IDE autocomplete and type checking:
|
package/cli/index.js
CHANGED
|
@@ -7,11 +7,14 @@
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { dirname, join, resolve } from 'path';
|
|
9
9
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync } from 'fs';
|
|
10
|
+
import { log } from './logger.js';
|
|
10
11
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = dirname(__filename);
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
// Version - read dynamically from package.json
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
17
|
+
const VERSION = pkg.version;
|
|
15
18
|
|
|
16
19
|
// Command handlers
|
|
17
20
|
const commands = {
|
|
@@ -38,8 +41,8 @@ async function main() {
|
|
|
38
41
|
if (command in commands) {
|
|
39
42
|
await commands[command](args.slice(1));
|
|
40
43
|
} else {
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
log.error(`Unknown command: ${command}`);
|
|
45
|
+
log.info('Run "pulse help" for usage information.');
|
|
43
46
|
process.exit(1);
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -48,7 +51,7 @@ async function main() {
|
|
|
48
51
|
* Show help message
|
|
49
52
|
*/
|
|
50
53
|
function showHelp() {
|
|
51
|
-
|
|
54
|
+
log.info(`
|
|
52
55
|
Pulse Framework CLI v${VERSION}
|
|
53
56
|
|
|
54
57
|
Usage: pulse <command> [options]
|
|
@@ -102,7 +105,7 @@ Documentation: https://github.com/vincenthirtz/pulse-js-framework
|
|
|
102
105
|
* Show version
|
|
103
106
|
*/
|
|
104
107
|
function showVersion() {
|
|
105
|
-
|
|
108
|
+
log.info(`Pulse Framework v${VERSION}`);
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
/**
|
|
@@ -112,19 +115,19 @@ async function createProject(args) {
|
|
|
112
115
|
const projectName = args[0];
|
|
113
116
|
|
|
114
117
|
if (!projectName) {
|
|
115
|
-
|
|
116
|
-
|
|
118
|
+
log.error('Please provide a project name.');
|
|
119
|
+
log.info('Usage: pulse create <project-name>');
|
|
117
120
|
process.exit(1);
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
const projectPath = resolve(process.cwd(), projectName);
|
|
121
124
|
|
|
122
125
|
if (existsSync(projectPath)) {
|
|
123
|
-
|
|
126
|
+
log.error(`Directory "${projectName}" already exists.`);
|
|
124
127
|
process.exit(1);
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
|
|
130
|
+
log.info(`Creating new Pulse project: ${projectName}`);
|
|
128
131
|
|
|
129
132
|
// Create project structure
|
|
130
133
|
mkdirSync(projectPath);
|
|
@@ -281,7 +284,7 @@ dist
|
|
|
281
284
|
|
|
282
285
|
writeFileSync(join(projectPath, '.gitignore'), gitignore);
|
|
283
286
|
|
|
284
|
-
|
|
287
|
+
log.info(`
|
|
285
288
|
Project created successfully!
|
|
286
289
|
|
|
287
290
|
Next steps:
|
|
@@ -297,7 +300,7 @@ Happy coding with Pulse!
|
|
|
297
300
|
* Run development server
|
|
298
301
|
*/
|
|
299
302
|
async function runDev(args) {
|
|
300
|
-
|
|
303
|
+
log.info('Starting Pulse development server...');
|
|
301
304
|
|
|
302
305
|
// Use dynamic import for the dev server module
|
|
303
306
|
const { startDevServer } = await import('./dev.js');
|
|
@@ -308,7 +311,7 @@ async function runDev(args) {
|
|
|
308
311
|
* Build for production
|
|
309
312
|
*/
|
|
310
313
|
async function runBuild(args) {
|
|
311
|
-
|
|
314
|
+
log.info('Building Pulse project for production...');
|
|
312
315
|
|
|
313
316
|
const { buildProject } = await import('./build.js');
|
|
314
317
|
await buildProject(args);
|
|
@@ -318,7 +321,7 @@ async function runBuild(args) {
|
|
|
318
321
|
* Preview production build
|
|
319
322
|
*/
|
|
320
323
|
async function runPreview(args) {
|
|
321
|
-
|
|
324
|
+
log.info('Starting Pulse preview server...');
|
|
322
325
|
|
|
323
326
|
const { previewBuild } = await import('./build.js');
|
|
324
327
|
await previewBuild(args);
|
|
@@ -363,13 +366,13 @@ async function compileFile(args) {
|
|
|
363
366
|
const inputFile = args[0];
|
|
364
367
|
|
|
365
368
|
if (!inputFile) {
|
|
366
|
-
|
|
367
|
-
|
|
369
|
+
log.error('Please provide a file to compile.');
|
|
370
|
+
log.info('Usage: pulse compile <file.pulse>');
|
|
368
371
|
process.exit(1);
|
|
369
372
|
}
|
|
370
373
|
|
|
371
374
|
if (!existsSync(inputFile)) {
|
|
372
|
-
|
|
375
|
+
log.error(`File not found: ${inputFile}`);
|
|
373
376
|
process.exit(1);
|
|
374
377
|
}
|
|
375
378
|
|
|
@@ -381,11 +384,11 @@ async function compileFile(args) {
|
|
|
381
384
|
if (result.success) {
|
|
382
385
|
const outputFile = inputFile.replace(/\.pulse$/, '.js');
|
|
383
386
|
writeFileSync(outputFile, result.code);
|
|
384
|
-
|
|
387
|
+
log.info(`Compiled: ${inputFile} -> ${outputFile}`);
|
|
385
388
|
} else {
|
|
386
|
-
|
|
389
|
+
log.error('Compilation failed:');
|
|
387
390
|
for (const error of result.errors) {
|
|
388
|
-
|
|
391
|
+
log.error(` ${error.message}`);
|
|
389
392
|
}
|
|
390
393
|
process.exit(1);
|
|
391
394
|
}
|
|
@@ -393,6 +396,6 @@ async function compileFile(args) {
|
|
|
393
396
|
|
|
394
397
|
// Run main
|
|
395
398
|
main().catch(error => {
|
|
396
|
-
|
|
399
|
+
log.error('Error:', error.message);
|
|
397
400
|
process.exit(1);
|
|
398
401
|
});
|
package/cli/logger.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI Logger
|
|
3
|
+
* Lightweight logger for CLI tools with support for verbose mode
|
|
4
|
+
* @module pulse-cli/logger
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @type {boolean} */
|
|
8
|
+
let verboseMode = false;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Enable or disable verbose mode for debug output
|
|
12
|
+
* @param {boolean} enabled - Whether to enable verbose mode
|
|
13
|
+
* @returns {void}
|
|
14
|
+
* @example
|
|
15
|
+
* setVerbose(true);
|
|
16
|
+
* log.debug('This will now be shown');
|
|
17
|
+
*/
|
|
18
|
+
export function setVerbose(enabled) {
|
|
19
|
+
verboseMode = enabled;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if verbose mode is currently enabled
|
|
24
|
+
* @returns {boolean} True if verbose mode is enabled
|
|
25
|
+
* @example
|
|
26
|
+
* if (isVerbose()) {
|
|
27
|
+
* // Perform additional logging
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
export function isVerbose() {
|
|
31
|
+
return verboseMode;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* CLI Logger object with console-like API
|
|
36
|
+
* @namespace log
|
|
37
|
+
*/
|
|
38
|
+
export const log = {
|
|
39
|
+
/**
|
|
40
|
+
* Log an info message (always shown)
|
|
41
|
+
* @param {...*} args - Values to log
|
|
42
|
+
* @returns {void}
|
|
43
|
+
* @example
|
|
44
|
+
* log.info('Starting server on port', 3000);
|
|
45
|
+
*/
|
|
46
|
+
info(...args) {
|
|
47
|
+
console.log(...args);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Log a success message (always shown)
|
|
52
|
+
* @param {...*} args - Values to log
|
|
53
|
+
* @returns {void}
|
|
54
|
+
* @example
|
|
55
|
+
* log.success('Build completed successfully!');
|
|
56
|
+
*/
|
|
57
|
+
success(...args) {
|
|
58
|
+
console.log(...args);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Log a warning message
|
|
63
|
+
* @param {...*} args - Values to log
|
|
64
|
+
* @returns {void}
|
|
65
|
+
* @example
|
|
66
|
+
* log.warn('Deprecated feature used');
|
|
67
|
+
*/
|
|
68
|
+
warn(...args) {
|
|
69
|
+
console.warn(...args);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Log an error message
|
|
74
|
+
* @param {...*} args - Values to log
|
|
75
|
+
* @returns {void}
|
|
76
|
+
* @example
|
|
77
|
+
* log.error('Failed to compile:', error.message);
|
|
78
|
+
*/
|
|
79
|
+
error(...args) {
|
|
80
|
+
console.error(...args);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Log a debug message (only shown in verbose mode)
|
|
85
|
+
* @param {...*} args - Values to log
|
|
86
|
+
* @returns {void}
|
|
87
|
+
* @example
|
|
88
|
+
* log.debug('Processing file:', filename);
|
|
89
|
+
*/
|
|
90
|
+
debug(...args) {
|
|
91
|
+
if (verboseMode) {
|
|
92
|
+
console.log('[debug]', ...args);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Log a verbose message (only shown in verbose mode)
|
|
98
|
+
* @param {...*} args - Values to log
|
|
99
|
+
* @returns {void}
|
|
100
|
+
* @example
|
|
101
|
+
* log.verbose('Additional details:', data);
|
|
102
|
+
*/
|
|
103
|
+
verbose(...args) {
|
|
104
|
+
if (verboseMode) {
|
|
105
|
+
console.log(...args);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Print a blank line for spacing
|
|
111
|
+
* @returns {void}
|
|
112
|
+
* @example
|
|
113
|
+
* log.info('Section 1');
|
|
114
|
+
* log.newline();
|
|
115
|
+
* log.info('Section 2');
|
|
116
|
+
*/
|
|
117
|
+
newline() {
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default log;
|
package/index.js
CHANGED
|
@@ -4,14 +4,20 @@
|
|
|
4
4
|
* A declarative DOM framework with CSS selector-based structure
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { readFileSync } from 'fs';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import { dirname, join } from 'path';
|
|
10
|
+
|
|
7
11
|
// Runtime exports
|
|
8
12
|
export * from './runtime/index.js';
|
|
9
13
|
|
|
10
14
|
// Compiler exports
|
|
11
15
|
export { compile, parse, tokenize } from './compiler/index.js';
|
|
12
16
|
|
|
13
|
-
// Version
|
|
14
|
-
|
|
17
|
+
// Version - read dynamically from package.json
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
20
|
+
export const VERSION = pkg.version;
|
|
15
21
|
|
|
16
22
|
// Default export
|
|
17
23
|
export default {
|
package/loader/vite-plugin.js
CHANGED
|
@@ -69,22 +69,30 @@ export default function pulsePlugin(options = {}) {
|
|
|
69
69
|
/**
|
|
70
70
|
* Handle hot module replacement
|
|
71
71
|
*/
|
|
72
|
-
handleHotUpdate({ file, server }) {
|
|
72
|
+
handleHotUpdate({ file, server, modules }) {
|
|
73
73
|
if (file.endsWith('.pulse')) {
|
|
74
74
|
console.log(`[Pulse] HMR update: ${file}`);
|
|
75
75
|
|
|
76
|
-
// Invalidate the module
|
|
76
|
+
// Invalidate the module in Vite's module graph
|
|
77
77
|
const module = server.moduleGraph.getModuleById(file);
|
|
78
78
|
if (module) {
|
|
79
79
|
server.moduleGraph.invalidateModule(module);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// Send full reload
|
|
83
|
-
//
|
|
82
|
+
// Send HMR update instead of full reload
|
|
83
|
+
// The module will handle its own state preservation via hmrRuntime
|
|
84
84
|
server.ws.send({
|
|
85
|
-
type: '
|
|
86
|
-
|
|
85
|
+
type: 'update',
|
|
86
|
+
updates: [{
|
|
87
|
+
type: 'js-update',
|
|
88
|
+
path: file,
|
|
89
|
+
acceptedPath: file,
|
|
90
|
+
timestamp: Date.now()
|
|
91
|
+
}]
|
|
87
92
|
});
|
|
93
|
+
|
|
94
|
+
// Return empty array to prevent Vite's default HMR handling
|
|
95
|
+
return [];
|
|
88
96
|
}
|
|
89
97
|
},
|
|
90
98
|
|
|
@@ -117,6 +125,14 @@ export default function pulsePlugin(options = {}) {
|
|
|
117
125
|
*/
|
|
118
126
|
export const hmrRuntime = `
|
|
119
127
|
if (import.meta.hot) {
|
|
128
|
+
// Cleanup effects before module replacement
|
|
129
|
+
import.meta.hot.dispose(() => {
|
|
130
|
+
import('pulse-js-framework/runtime/pulse').then(m => {
|
|
131
|
+
m.disposeModule(import.meta.url);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Accept HMR updates
|
|
120
136
|
import.meta.hot.accept((newModule) => {
|
|
121
137
|
if (newModule) {
|
|
122
138
|
// Re-render with new module
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
"types": "./types/index.d.ts",
|
|
18
18
|
"default": "./runtime/index.js"
|
|
19
19
|
},
|
|
20
|
+
"./runtime/pulse": {
|
|
21
|
+
"types": "./types/pulse.d.ts",
|
|
22
|
+
"default": "./runtime/pulse.js"
|
|
23
|
+
},
|
|
24
|
+
"./runtime/dom": {
|
|
25
|
+
"types": "./types/dom.d.ts",
|
|
26
|
+
"default": "./runtime/dom.js"
|
|
27
|
+
},
|
|
20
28
|
"./runtime/router": {
|
|
21
29
|
"types": "./types/router.d.ts",
|
|
22
30
|
"default": "./runtime/router.js"
|
|
@@ -25,9 +33,30 @@
|
|
|
25
33
|
"types": "./types/store.d.ts",
|
|
26
34
|
"default": "./runtime/store.js"
|
|
27
35
|
},
|
|
28
|
-
"./runtime
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
"./runtime/native": {
|
|
37
|
+
"types": "./types/index.d.ts",
|
|
38
|
+
"default": "./runtime/native.js"
|
|
39
|
+
},
|
|
40
|
+
"./runtime/logger": {
|
|
41
|
+
"types": "./types/logger.d.ts",
|
|
42
|
+
"default": "./runtime/logger.js"
|
|
43
|
+
},
|
|
44
|
+
"./runtime/hmr": {
|
|
45
|
+
"default": "./runtime/hmr.js"
|
|
46
|
+
},
|
|
47
|
+
"./compiler": {
|
|
48
|
+
"types": "./types/index.d.ts",
|
|
49
|
+
"default": "./compiler/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./compiler/lexer": "./compiler/lexer.js",
|
|
52
|
+
"./compiler/parser": "./compiler/parser.js",
|
|
53
|
+
"./compiler/transformer": "./compiler/transformer.js",
|
|
54
|
+
"./vite": {
|
|
55
|
+
"types": "./types/index.d.ts",
|
|
56
|
+
"default": "./loader/vite-plugin.js"
|
|
57
|
+
},
|
|
58
|
+
"./mobile": "./mobile/bridge/pulse-native.js",
|
|
59
|
+
"./package.json": "./package.json"
|
|
31
60
|
},
|
|
32
61
|
"files": [
|
|
33
62
|
"index.js",
|
|
@@ -41,16 +70,18 @@
|
|
|
41
70
|
"LICENSE"
|
|
42
71
|
],
|
|
43
72
|
"scripts": {
|
|
44
|
-
"test": "npm run test:compiler && npm run test:pulse && npm run test:dom && npm run test:router && npm run test:store && npm run test:lint && npm run test:format && npm run test:analyze",
|
|
73
|
+
"test": "npm run test:compiler && npm run test:pulse && npm run test:dom && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze",
|
|
45
74
|
"test:compiler": "node test/compiler.test.js",
|
|
46
75
|
"test:pulse": "node test/pulse.test.js",
|
|
47
76
|
"test:dom": "node test/dom.test.js",
|
|
48
77
|
"test:router": "node test/router.test.js",
|
|
49
78
|
"test:store": "node test/store.test.js",
|
|
79
|
+
"test:hmr": "node test/hmr.test.js",
|
|
50
80
|
"test:lint": "node test/lint.test.js",
|
|
51
81
|
"test:format": "node test/format.test.js",
|
|
52
82
|
"test:analyze": "node test/analyze.test.js",
|
|
53
|
-
"build:netlify": "node scripts/build-netlify.js"
|
|
83
|
+
"build:netlify": "node scripts/build-netlify.js",
|
|
84
|
+
"version": "node scripts/sync-version.js"
|
|
54
85
|
},
|
|
55
86
|
"keywords": [
|
|
56
87
|
"framework",
|
package/runtime/dom.js
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { effect, pulse, batch, onCleanup } from './pulse.js';
|
|
9
|
+
import { loggers } from './logger.js';
|
|
10
|
+
|
|
11
|
+
const log = loggers.dom;
|
|
12
|
+
|
|
13
|
+
// Selector cache for parseSelector
|
|
14
|
+
const selectorCache = new Map();
|
|
15
|
+
const SELECTOR_CACHE_MAX = 500;
|
|
9
16
|
|
|
10
17
|
// Lifecycle tracking
|
|
11
18
|
let mountCallbacks = [];
|
|
@@ -38,6 +45,7 @@ export function onUnmount(fn) {
|
|
|
38
45
|
/**
|
|
39
46
|
* Parse a CSS selector-like string into element configuration
|
|
40
47
|
* Supports: tag, #id, .class, [attr=value]
|
|
48
|
+
* Results are cached for performance.
|
|
41
49
|
*
|
|
42
50
|
* Examples:
|
|
43
51
|
* "div" -> { tag: "div" }
|
|
@@ -47,6 +55,22 @@ export function onUnmount(fn) {
|
|
|
47
55
|
* "input[type=text][placeholder=Name]" -> { tag: "input", attrs: { type: "text", placeholder: "Name" } }
|
|
48
56
|
*/
|
|
49
57
|
export function parseSelector(selector) {
|
|
58
|
+
if (!selector || selector === '') {
|
|
59
|
+
return { tag: 'div', id: null, classes: [], attrs: {} };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check cache first
|
|
63
|
+
const cached = selectorCache.get(selector);
|
|
64
|
+
if (cached) {
|
|
65
|
+
// Return a shallow copy to prevent mutation
|
|
66
|
+
return {
|
|
67
|
+
tag: cached.tag,
|
|
68
|
+
id: cached.id,
|
|
69
|
+
classes: [...cached.classes],
|
|
70
|
+
attrs: { ...cached.attrs }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
const config = {
|
|
51
75
|
tag: 'div',
|
|
52
76
|
id: null,
|
|
@@ -54,30 +78,30 @@ export function parseSelector(selector) {
|
|
|
54
78
|
attrs: {}
|
|
55
79
|
};
|
|
56
80
|
|
|
57
|
-
|
|
81
|
+
let remaining = selector;
|
|
58
82
|
|
|
59
83
|
// Match tag name at the start
|
|
60
|
-
const tagMatch =
|
|
84
|
+
const tagMatch = remaining.match(/^([a-zA-Z][a-zA-Z0-9-]*)/);
|
|
61
85
|
if (tagMatch) {
|
|
62
86
|
config.tag = tagMatch[1];
|
|
63
|
-
|
|
87
|
+
remaining = remaining.slice(tagMatch[0].length);
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
// Match ID
|
|
67
|
-
const idMatch =
|
|
91
|
+
const idMatch = remaining.match(/#([a-zA-Z][a-zA-Z0-9-_]*)/);
|
|
68
92
|
if (idMatch) {
|
|
69
93
|
config.id = idMatch[1];
|
|
70
|
-
|
|
94
|
+
remaining = remaining.replace(idMatch[0], '');
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
// Match classes
|
|
74
|
-
const classMatches =
|
|
98
|
+
const classMatches = remaining.matchAll(/\.([a-zA-Z][a-zA-Z0-9-_]*)/g);
|
|
75
99
|
for (const match of classMatches) {
|
|
76
100
|
config.classes.push(match[1]);
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
// Match attributes
|
|
80
|
-
const attrMatches =
|
|
104
|
+
const attrMatches = remaining.matchAll(/\[([a-zA-Z][a-zA-Z0-9-_]*)(?:=([^\]]+))?\]/g);
|
|
81
105
|
for (const match of attrMatches) {
|
|
82
106
|
const key = match[1];
|
|
83
107
|
let value = match[2] || '';
|
|
@@ -89,7 +113,21 @@ export function parseSelector(selector) {
|
|
|
89
113
|
config.attrs[key] = value;
|
|
90
114
|
}
|
|
91
115
|
|
|
92
|
-
|
|
116
|
+
// Cache the result (with size limit to prevent memory leaks)
|
|
117
|
+
if (selectorCache.size >= SELECTOR_CACHE_MAX) {
|
|
118
|
+
// Remove oldest entry (first key)
|
|
119
|
+
const firstKey = selectorCache.keys().next().value;
|
|
120
|
+
selectorCache.delete(firstKey);
|
|
121
|
+
}
|
|
122
|
+
selectorCache.set(selector, config);
|
|
123
|
+
|
|
124
|
+
// Return a copy
|
|
125
|
+
return {
|
|
126
|
+
tag: config.tag,
|
|
127
|
+
id: config.id,
|
|
128
|
+
classes: [...config.classes],
|
|
129
|
+
attrs: { ...config.attrs }
|
|
130
|
+
};
|
|
93
131
|
}
|
|
94
132
|
|
|
95
133
|
/**
|
|
@@ -262,6 +300,12 @@ export function style(element, prop, getValue) {
|
|
|
262
300
|
*/
|
|
263
301
|
export function on(element, event, handler, options) {
|
|
264
302
|
element.addEventListener(event, handler, options);
|
|
303
|
+
|
|
304
|
+
// Auto-cleanup: remove listener when effect is disposed (HMR support)
|
|
305
|
+
onCleanup(() => {
|
|
306
|
+
element.removeEventListener(event, handler, options);
|
|
307
|
+
});
|
|
308
|
+
|
|
265
309
|
return element;
|
|
266
310
|
}
|
|
267
311
|
|
|
@@ -523,7 +567,7 @@ export function component(setup) {
|
|
|
523
567
|
try {
|
|
524
568
|
cb();
|
|
525
569
|
} catch (e) {
|
|
526
|
-
|
|
570
|
+
log.error('Mount callback error:', e);
|
|
527
571
|
}
|
|
528
572
|
}
|
|
529
573
|
});
|
|
@@ -559,7 +603,7 @@ export function portal(children, target) {
|
|
|
559
603
|
: target;
|
|
560
604
|
|
|
561
605
|
if (!resolvedTarget) {
|
|
562
|
-
|
|
606
|
+
log.warn('Portal target not found:', target);
|
|
563
607
|
return document.createComment('portal-target-not-found');
|
|
564
608
|
}
|
|
565
609
|
|
|
@@ -653,7 +697,7 @@ export function errorBoundary(children, fallback) {
|
|
|
653
697
|
marker.parentNode?.insertBefore(fragment, marker.nextSibling);
|
|
654
698
|
}
|
|
655
699
|
} catch (e) {
|
|
656
|
-
|
|
700
|
+
log.error('Error in component:', e);
|
|
657
701
|
error.set(e);
|
|
658
702
|
// Re-render with error
|
|
659
703
|
if (!hasError) {
|