pulse-js-framework 1.0.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/LICENSE +21 -0
- package/README.md +182 -0
- package/cli/build.js +199 -0
- package/cli/dev.js +225 -0
- package/cli/index.js +324 -0
- package/compiler/index.js +65 -0
- package/compiler/lexer.js +581 -0
- package/compiler/parser.js +900 -0
- package/compiler/transformer.js +552 -0
- package/index.js +19 -0
- package/loader/vite-plugin.js +160 -0
- package/package.json +58 -0
- package/runtime/dom.js +484 -0
- package/runtime/index.js +13 -0
- package/runtime/pulse.js +339 -0
- package/runtime/router.js +392 -0
- package/runtime/store.js +301 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Pulse Framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Pulse Framework
|
|
2
|
+
|
|
3
|
+
A declarative DOM framework with CSS selector-based structure and reactive pulsations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **CSS Selector Syntax** - Create DOM elements using familiar CSS selectors
|
|
8
|
+
- **Reactive Pulsations** - Automatic UI updates when state changes
|
|
9
|
+
- **Custom DSL** - Optional `.pulse` file format for cleaner code
|
|
10
|
+
- **No Build Required** - Works directly in the browser
|
|
11
|
+
- **Lightweight** - Minimal footprint, maximum performance
|
|
12
|
+
- **Router & Store** - Built-in SPA routing and state management
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install pulse-js-framework
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### Create a new project
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx pulse create my-app
|
|
26
|
+
cd my-app
|
|
27
|
+
npm install
|
|
28
|
+
npm run dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Or use directly
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { pulse, effect, el, mount } from 'pulse-js-framework';
|
|
35
|
+
|
|
36
|
+
// Create reactive state
|
|
37
|
+
const count = pulse(0);
|
|
38
|
+
|
|
39
|
+
// Build UI with CSS selector syntax
|
|
40
|
+
function Counter() {
|
|
41
|
+
const div = el('.counter');
|
|
42
|
+
|
|
43
|
+
const display = el('h1');
|
|
44
|
+
effect(() => {
|
|
45
|
+
display.textContent = `Count: ${count.get()}`;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const increment = el('button.btn', '+');
|
|
49
|
+
increment.onclick = () => count.update(n => n + 1);
|
|
50
|
+
|
|
51
|
+
const decrement = el('button.btn', '-');
|
|
52
|
+
decrement.onclick = () => count.update(n => n - 1);
|
|
53
|
+
|
|
54
|
+
div.append(display, increment, decrement);
|
|
55
|
+
return div;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
mount('#app', Counter());
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## CSS Selector Syntax
|
|
62
|
+
|
|
63
|
+
Create DOM elements using familiar CSS syntax:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
el('div') // <div></div>
|
|
67
|
+
el('.container') // <div class="container"></div>
|
|
68
|
+
el('#app') // <div id="app"></div>
|
|
69
|
+
el('button.btn.primary') // <button class="btn primary"></button>
|
|
70
|
+
el('input[type=text]') // <input type="text">
|
|
71
|
+
el('h1', 'Hello World') // <h1>Hello World</h1>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Reactivity
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
import { pulse, effect, computed } from 'pulse-js-framework';
|
|
78
|
+
|
|
79
|
+
// Create reactive values
|
|
80
|
+
const firstName = pulse('John');
|
|
81
|
+
const lastName = pulse('Doe');
|
|
82
|
+
|
|
83
|
+
// Computed values
|
|
84
|
+
const fullName = computed(() =>
|
|
85
|
+
`${firstName.get()} ${lastName.get()}`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Effects auto-run when dependencies change
|
|
89
|
+
effect(() => {
|
|
90
|
+
console.log(`Hello, ${fullName.get()}!`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
firstName.set('Jane'); // Logs: "Hello, Jane Doe!"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## .pulse File Format
|
|
97
|
+
|
|
98
|
+
```pulse
|
|
99
|
+
@page Counter
|
|
100
|
+
|
|
101
|
+
state {
|
|
102
|
+
count: 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
view {
|
|
106
|
+
.counter {
|
|
107
|
+
h1 "Count: {count}"
|
|
108
|
+
button @click(count++) "+"
|
|
109
|
+
button @click(count--) "-"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
style {
|
|
114
|
+
.counter {
|
|
115
|
+
text-align: center
|
|
116
|
+
padding: 20px
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## API Reference
|
|
122
|
+
|
|
123
|
+
### Reactivity
|
|
124
|
+
|
|
125
|
+
- `pulse(value)` - Create a reactive value
|
|
126
|
+
- `pulse.get()` - Read value (tracks dependency)
|
|
127
|
+
- `pulse.set(value)` - Set new value
|
|
128
|
+
- `pulse.update(fn)` - Update with function
|
|
129
|
+
- `pulse.peek()` - Read without tracking
|
|
130
|
+
- `effect(fn)` - Create reactive side effect
|
|
131
|
+
- `computed(fn)` - Create derived value
|
|
132
|
+
- `batch(fn)` - Batch multiple updates
|
|
133
|
+
|
|
134
|
+
### DOM
|
|
135
|
+
|
|
136
|
+
- `el(selector, ...children)` - Create element
|
|
137
|
+
- `mount(target, element)` - Mount to DOM
|
|
138
|
+
- `text(fn)` - Reactive text node
|
|
139
|
+
- `list(items, template)` - Reactive list
|
|
140
|
+
- `when(condition, then, else)` - Conditional render
|
|
141
|
+
|
|
142
|
+
### Router
|
|
143
|
+
|
|
144
|
+
```javascript
|
|
145
|
+
import { createRouter } from 'pulse-js-framework/runtime/router.js';
|
|
146
|
+
|
|
147
|
+
const router = createRouter({
|
|
148
|
+
routes: {
|
|
149
|
+
'/': HomePage,
|
|
150
|
+
'/about': AboutPage,
|
|
151
|
+
'/users/:id': UserPage
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
router.start();
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Store
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
import { createStore } from 'pulse-js-framework/runtime/store.js';
|
|
162
|
+
|
|
163
|
+
const store = createStore({
|
|
164
|
+
user: null,
|
|
165
|
+
theme: 'light'
|
|
166
|
+
}, { persist: true });
|
|
167
|
+
|
|
168
|
+
store.user.set({ name: 'John' });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## CLI Commands
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pulse create <name> # Create new project
|
|
175
|
+
pulse dev [port] # Start dev server
|
|
176
|
+
pulse build # Build for production
|
|
177
|
+
pulse compile <file> # Compile .pulse file
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
package/cli/build.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Build System
|
|
3
|
+
*
|
|
4
|
+
* Builds Pulse projects for production
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, copyFileSync } from 'fs';
|
|
8
|
+
import { join, extname, relative, dirname } from 'path';
|
|
9
|
+
import { compile } from '../compiler/index.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build project for production
|
|
13
|
+
*/
|
|
14
|
+
export async function buildProject(args) {
|
|
15
|
+
const root = process.cwd();
|
|
16
|
+
const outDir = join(root, 'dist');
|
|
17
|
+
|
|
18
|
+
// Check if vite is available
|
|
19
|
+
try {
|
|
20
|
+
const viteConfig = join(root, 'vite.config.js');
|
|
21
|
+
if (existsSync(viteConfig)) {
|
|
22
|
+
console.log('Vite config detected, using Vite build...');
|
|
23
|
+
const { build } = await import('vite');
|
|
24
|
+
await build({ root });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Vite not available, use built-in build
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log('Building with Pulse compiler...');
|
|
32
|
+
|
|
33
|
+
// Create output directory
|
|
34
|
+
if (!existsSync(outDir)) {
|
|
35
|
+
mkdirSync(outDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Copy public files
|
|
39
|
+
const publicDir = join(root, 'public');
|
|
40
|
+
if (existsSync(publicDir)) {
|
|
41
|
+
copyDir(publicDir, outDir);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Process source files
|
|
45
|
+
const srcDir = join(root, 'src');
|
|
46
|
+
if (existsSync(srcDir)) {
|
|
47
|
+
processDirectory(srcDir, join(outDir, 'assets'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Copy and process index.html
|
|
51
|
+
const indexHtml = join(root, 'index.html');
|
|
52
|
+
if (existsSync(indexHtml)) {
|
|
53
|
+
let html = readFileSync(indexHtml, 'utf-8');
|
|
54
|
+
|
|
55
|
+
// Update script paths for production
|
|
56
|
+
html = html.replace(
|
|
57
|
+
/src="\/src\/([^"]+)"/g,
|
|
58
|
+
'src="/assets/$1"'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Rewrite .pulse imports to .js
|
|
62
|
+
html = html.replace(/\.pulse"/g, '.js"');
|
|
63
|
+
|
|
64
|
+
writeFileSync(join(outDir, 'index.html'), html);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Bundle runtime
|
|
68
|
+
bundleRuntime(outDir);
|
|
69
|
+
|
|
70
|
+
console.log(`
|
|
71
|
+
Build complete!
|
|
72
|
+
|
|
73
|
+
Output directory: ${relative(root, outDir)}
|
|
74
|
+
|
|
75
|
+
To preview the build:
|
|
76
|
+
npx serve dist
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Process a directory of source files
|
|
82
|
+
*/
|
|
83
|
+
function processDirectory(srcDir, outDir) {
|
|
84
|
+
if (!existsSync(outDir)) {
|
|
85
|
+
mkdirSync(outDir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const files = readdirSync(srcDir);
|
|
89
|
+
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
const srcPath = join(srcDir, file);
|
|
92
|
+
const stat = statSync(srcPath);
|
|
93
|
+
|
|
94
|
+
if (stat.isDirectory()) {
|
|
95
|
+
processDirectory(srcPath, join(outDir, file));
|
|
96
|
+
} else if (file.endsWith('.pulse')) {
|
|
97
|
+
// Compile .pulse files
|
|
98
|
+
const source = readFileSync(srcPath, 'utf-8');
|
|
99
|
+
const result = compile(source, {
|
|
100
|
+
runtime: './runtime.js',
|
|
101
|
+
minify: true
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (result.success) {
|
|
105
|
+
const outPath = join(outDir, file.replace('.pulse', '.js'));
|
|
106
|
+
writeFileSync(outPath, result.code);
|
|
107
|
+
console.log(` Compiled: ${file}`);
|
|
108
|
+
} else {
|
|
109
|
+
console.error(` Error compiling ${file}:`);
|
|
110
|
+
for (const error of result.errors) {
|
|
111
|
+
console.error(` ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} else if (file.endsWith('.js') || file.endsWith('.mjs')) {
|
|
115
|
+
// Process JS files - rewrite imports
|
|
116
|
+
let content = readFileSync(srcPath, 'utf-8');
|
|
117
|
+
|
|
118
|
+
// Rewrite .pulse imports to .js
|
|
119
|
+
content = content.replace(/from\s+['"]([^'"]+)\.pulse['"]/g, "from '$1.js'");
|
|
120
|
+
|
|
121
|
+
// Rewrite runtime imports
|
|
122
|
+
content = content.replace(
|
|
123
|
+
/from\s+['"]pulse-framework\/runtime['"]/g,
|
|
124
|
+
"from './runtime.js'"
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const outPath = join(outDir, file);
|
|
128
|
+
writeFileSync(outPath, content);
|
|
129
|
+
console.log(` Processed: ${file}`);
|
|
130
|
+
} else {
|
|
131
|
+
// Copy other files
|
|
132
|
+
const outPath = join(outDir, file);
|
|
133
|
+
copyFileSync(srcPath, outPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Bundle the runtime into a single file
|
|
140
|
+
*/
|
|
141
|
+
function bundleRuntime(outDir) {
|
|
142
|
+
// For simplicity, we'll create a minimal runtime bundle
|
|
143
|
+
// In production, you'd want to use a proper bundler
|
|
144
|
+
|
|
145
|
+
const runtimeCode = `
|
|
146
|
+
// Pulse Runtime (bundled)
|
|
147
|
+
${readRuntimeFile('pulse.js')}
|
|
148
|
+
${readRuntimeFile('dom.js')}
|
|
149
|
+
${readRuntimeFile('router.js')}
|
|
150
|
+
${readRuntimeFile('store.js')}
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
writeFileSync(join(outDir, 'assets', 'runtime.js'), runtimeCode);
|
|
154
|
+
console.log(' Bundled: runtime.js');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Read a runtime file
|
|
159
|
+
*/
|
|
160
|
+
function readRuntimeFile(filename) {
|
|
161
|
+
const paths = [
|
|
162
|
+
join(process.cwd(), 'node_modules', 'pulse-framework', 'runtime', filename),
|
|
163
|
+
join(dirname(new URL(import.meta.url).pathname), '..', 'runtime', filename)
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
for (const path of paths) {
|
|
167
|
+
if (existsSync(path)) {
|
|
168
|
+
return readFileSync(path, 'utf-8');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
console.warn(` Warning: Could not find runtime file: ${filename}`);
|
|
173
|
+
return '';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Copy a directory recursively
|
|
178
|
+
*/
|
|
179
|
+
function copyDir(src, dest) {
|
|
180
|
+
if (!existsSync(dest)) {
|
|
181
|
+
mkdirSync(dest, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const files = readdirSync(src);
|
|
185
|
+
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
const srcPath = join(src, file);
|
|
188
|
+
const destPath = join(dest, file);
|
|
189
|
+
const stat = statSync(srcPath);
|
|
190
|
+
|
|
191
|
+
if (stat.isDirectory()) {
|
|
192
|
+
copyDir(srcPath, destPath);
|
|
193
|
+
} else {
|
|
194
|
+
copyFileSync(srcPath, destPath);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default { buildProject };
|
package/cli/dev.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Development Server
|
|
3
|
+
*
|
|
4
|
+
* A simple development server with hot module replacement
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createServer } from 'http';
|
|
8
|
+
import { readFileSync, existsSync, statSync, watch } from 'fs';
|
|
9
|
+
import { join, extname, resolve } from 'path';
|
|
10
|
+
import { compile } from '../compiler/index.js';
|
|
11
|
+
|
|
12
|
+
const MIME_TYPES = {
|
|
13
|
+
'.html': 'text/html',
|
|
14
|
+
'.js': 'application/javascript',
|
|
15
|
+
'.mjs': 'application/javascript',
|
|
16
|
+
'.css': 'text/css',
|
|
17
|
+
'.json': 'application/json',
|
|
18
|
+
'.png': 'image/png',
|
|
19
|
+
'.jpg': 'image/jpeg',
|
|
20
|
+
'.gif': 'image/gif',
|
|
21
|
+
'.svg': 'image/svg+xml',
|
|
22
|
+
'.ico': 'image/x-icon',
|
|
23
|
+
'.woff': 'font/woff',
|
|
24
|
+
'.woff2': 'font/woff2'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Connected clients for HMR
|
|
28
|
+
const clients = new Set();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Start the development server
|
|
32
|
+
*/
|
|
33
|
+
export async function startDevServer(args) {
|
|
34
|
+
const port = parseInt(args[0]) || 3000;
|
|
35
|
+
const root = process.cwd();
|
|
36
|
+
|
|
37
|
+
// Check if vite is available, use it if so
|
|
38
|
+
try {
|
|
39
|
+
const viteConfig = join(root, 'vite.config.js');
|
|
40
|
+
if (existsSync(viteConfig)) {
|
|
41
|
+
console.log('Vite config detected, using Vite...');
|
|
42
|
+
const { createServer: createViteServer } = await import('vite');
|
|
43
|
+
const server = await createViteServer({
|
|
44
|
+
root,
|
|
45
|
+
server: { port }
|
|
46
|
+
});
|
|
47
|
+
await server.listen();
|
|
48
|
+
server.printUrls();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Vite not available, use built-in server
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Built-in development server
|
|
56
|
+
const server = createServer(async (req, res) => {
|
|
57
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
58
|
+
let pathname = url.pathname;
|
|
59
|
+
|
|
60
|
+
// Handle HMR WebSocket upgrade
|
|
61
|
+
if (pathname === '/__pulse_hmr') {
|
|
62
|
+
// This would need WebSocket support
|
|
63
|
+
res.writeHead(200);
|
|
64
|
+
res.end();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Serve index.html for root
|
|
69
|
+
if (pathname === '/') {
|
|
70
|
+
pathname = '/index.html';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Try to serve the file
|
|
74
|
+
let filePath = join(root, pathname);
|
|
75
|
+
|
|
76
|
+
// Check for .pulse files and compile them
|
|
77
|
+
if (pathname.endsWith('.pulse')) {
|
|
78
|
+
if (existsSync(filePath)) {
|
|
79
|
+
try {
|
|
80
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
81
|
+
const result = compile(source, {
|
|
82
|
+
runtime: '/node_modules/pulse-framework/runtime/index.js'
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (result.success) {
|
|
86
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
87
|
+
res.end(result.code);
|
|
88
|
+
} else {
|
|
89
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
90
|
+
res.end(`Compilation error: ${result.errors.map(e => e.message).join('\n')}`);
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
94
|
+
res.end(`Error: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Serve JS files
|
|
101
|
+
if (pathname.endsWith('.js') || pathname.endsWith('.mjs')) {
|
|
102
|
+
if (existsSync(filePath)) {
|
|
103
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
104
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
105
|
+
res.end(content);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Handle node_modules
|
|
111
|
+
if (pathname.startsWith('/node_modules/pulse-framework/')) {
|
|
112
|
+
const modulePath = join(root, '..', 'pulse', pathname.replace('/node_modules/pulse-framework/', ''));
|
|
113
|
+
if (existsSync(modulePath)) {
|
|
114
|
+
const content = readFileSync(modulePath, 'utf-8');
|
|
115
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
116
|
+
res.end(content);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle runtime path (for examples)
|
|
122
|
+
if (pathname.includes('/runtime/')) {
|
|
123
|
+
const runtimeFile = pathname.split('/runtime/')[1] || 'index.js';
|
|
124
|
+
|
|
125
|
+
// Try multiple possible locations
|
|
126
|
+
const possiblePaths = [
|
|
127
|
+
join(root, '..', 'runtime', runtimeFile), // pulse/example/ -> pulse/runtime/
|
|
128
|
+
join(root, '..', '..', 'runtime', runtimeFile), // pulse/examples/*/ -> pulse/runtime/
|
|
129
|
+
join(root, 'runtime', runtimeFile), // pulse/docs/ -> pulse/docs/runtime/ (fallback)
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
for (const runtimePath of possiblePaths) {
|
|
133
|
+
if (existsSync(runtimePath)) {
|
|
134
|
+
const content = readFileSync(runtimePath, 'utf-8');
|
|
135
|
+
res.writeHead(200, { 'Content-Type': 'application/javascript' });
|
|
136
|
+
res.end(content);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Serve static files
|
|
143
|
+
if (existsSync(filePath) && statSync(filePath).isFile()) {
|
|
144
|
+
const ext = extname(filePath);
|
|
145
|
+
const mimeType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
146
|
+
|
|
147
|
+
res.writeHead(200, { 'Content-Type': mimeType });
|
|
148
|
+
res.end(readFileSync(filePath));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 404
|
|
153
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
154
|
+
res.end('Not Found');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Watch for file changes
|
|
158
|
+
watchFiles(root);
|
|
159
|
+
|
|
160
|
+
server.listen(port, () => {
|
|
161
|
+
console.log(`
|
|
162
|
+
Pulse Dev Server running at:
|
|
163
|
+
|
|
164
|
+
Local: http://localhost:${port}/
|
|
165
|
+
|
|
166
|
+
Press Ctrl+C to stop.
|
|
167
|
+
`);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Watch files for changes
|
|
173
|
+
*/
|
|
174
|
+
function watchFiles(root) {
|
|
175
|
+
const srcDir = join(root, 'src');
|
|
176
|
+
|
|
177
|
+
if (existsSync(srcDir)) {
|
|
178
|
+
watch(srcDir, { recursive: true }, (eventType, filename) => {
|
|
179
|
+
if (filename && filename.endsWith('.pulse')) {
|
|
180
|
+
console.log(`File changed: ${filename}`);
|
|
181
|
+
// Notify HMR clients (simplified)
|
|
182
|
+
notifyClients({ type: 'update', path: `/src/${filename}` });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Notify connected HMR clients
|
|
190
|
+
*/
|
|
191
|
+
function notifyClients(message) {
|
|
192
|
+
for (const client of clients) {
|
|
193
|
+
try {
|
|
194
|
+
client.send(JSON.stringify(message));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
clients.delete(client);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get HMR client code
|
|
203
|
+
*/
|
|
204
|
+
function getHMRClient() {
|
|
205
|
+
return `
|
|
206
|
+
(function() {
|
|
207
|
+
const ws = new WebSocket('ws://' + location.host + '/__pulse_hmr');
|
|
208
|
+
|
|
209
|
+
ws.onmessage = function(event) {
|
|
210
|
+
const data = JSON.parse(event.data);
|
|
211
|
+
if (data.type === 'update') {
|
|
212
|
+
console.log('[Pulse HMR] Update:', data.path);
|
|
213
|
+
location.reload();
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
ws.onclose = function() {
|
|
218
|
+
console.log('[Pulse HMR] Connection lost, reconnecting...');
|
|
219
|
+
setTimeout(() => location.reload(), 1000);
|
|
220
|
+
};
|
|
221
|
+
})();
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default { startDevServer };
|