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 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 };