pulse-js-framework 1.7.15 → 1.7.17
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 +43 -0
- package/cli/help.js +617 -0
- package/cli/index.js +102 -106
- package/cli/utils/file-utils.js +26 -4
- package/package.json +3 -2
- package/runtime/async.js +39 -0
- package/runtime/dom-element.js +107 -0
- package/runtime/index.js +70 -6
- package/runtime/pulse.js +40 -0
- package/runtime/ssr-async.js +229 -0
- package/runtime/ssr-hydrator.js +310 -0
- package/runtime/ssr-serializer.js +266 -0
- package/runtime/ssr.js +463 -0
package/cli/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { dirname, join, resolve, relative } from 'path';
|
|
|
9
9
|
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, watch } from 'fs';
|
|
10
10
|
import { log } from './logger.js';
|
|
11
11
|
import { findPulseFiles, parseArgs } from './utils/file-utils.js';
|
|
12
|
+
import { runHelp } from './help.js';
|
|
12
13
|
|
|
13
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
15
|
const __dirname = dirname(__filename);
|
|
@@ -23,6 +24,7 @@ const commands = {
|
|
|
23
24
|
version: showVersion,
|
|
24
25
|
create: createProject,
|
|
25
26
|
init: initProject,
|
|
27
|
+
new: newPulseFile,
|
|
26
28
|
dev: runDev,
|
|
27
29
|
build: runBuild,
|
|
28
30
|
preview: runPreview,
|
|
@@ -72,7 +74,9 @@ const commandAliases = {
|
|
|
72
74
|
'scaffod': 'scaffold',
|
|
73
75
|
'scafflod': 'scaffold',
|
|
74
76
|
'doc': 'docs',
|
|
75
|
-
'dcos': 'docs'
|
|
77
|
+
'dcos': 'docs',
|
|
78
|
+
'nwe': 'new',
|
|
79
|
+
'enw': 'new'
|
|
76
80
|
};
|
|
77
81
|
|
|
78
82
|
/**
|
|
@@ -136,10 +140,28 @@ function suggestCommand(input) {
|
|
|
136
140
|
*/
|
|
137
141
|
async function main() {
|
|
138
142
|
const args = process.argv.slice(2);
|
|
139
|
-
|
|
143
|
+
let command = args[0] || 'help';
|
|
144
|
+
|
|
145
|
+
// Handle global --help and -h flags
|
|
146
|
+
if (command === '--help' || command === '-h') {
|
|
147
|
+
command = 'help';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle --version and -v flags
|
|
151
|
+
if (command === '--version' || command === '-v') {
|
|
152
|
+
command = 'version';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle command-specific help: pulse <cmd> --help or pulse <cmd> -h
|
|
156
|
+
const cmdArgs = args.slice(1);
|
|
157
|
+
if (cmdArgs.includes('--help') || cmdArgs.includes('-h')) {
|
|
158
|
+
// Show help for the specific command
|
|
159
|
+
await commands.help([command]);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
140
162
|
|
|
141
163
|
if (command in commands) {
|
|
142
|
-
await commands[command](
|
|
164
|
+
await commands[command](cmdArgs);
|
|
143
165
|
} else {
|
|
144
166
|
log.error(`Unknown command: ${command}`);
|
|
145
167
|
|
|
@@ -156,110 +178,10 @@ async function main() {
|
|
|
156
178
|
|
|
157
179
|
/**
|
|
158
180
|
* Show help message
|
|
181
|
+
* Supports: pulse help, pulse help <command>
|
|
159
182
|
*/
|
|
160
|
-
function showHelp() {
|
|
161
|
-
|
|
162
|
-
Pulse Framework CLI v${VERSION}
|
|
163
|
-
|
|
164
|
-
Usage: pulse <command> [options]
|
|
165
|
-
|
|
166
|
-
Commands:
|
|
167
|
-
create <name> Create a new Pulse project
|
|
168
|
-
init [options] Initialize project in current directory
|
|
169
|
-
dev [port] Start development server (default: 3000)
|
|
170
|
-
build Build for production (minified)
|
|
171
|
-
preview [port] Preview production build (default: 4173)
|
|
172
|
-
compile <file> Compile a .pulse file to JavaScript
|
|
173
|
-
mobile <cmd> Mobile app commands (init, build, run)
|
|
174
|
-
lint [files] Validate .pulse files for errors and style
|
|
175
|
-
format [files] Format .pulse files consistently
|
|
176
|
-
analyze Analyze bundle size and dependencies
|
|
177
|
-
test [files] Run tests with coverage support
|
|
178
|
-
doctor Run project diagnostics
|
|
179
|
-
scaffold <type> Generate components, pages, stores
|
|
180
|
-
docs Generate API documentation from JSDoc
|
|
181
|
-
release <type> Create a new release (patch, minor, major)
|
|
182
|
-
docs-test Test documentation (syntax, imports, HTTP)
|
|
183
|
-
version Show version number
|
|
184
|
-
help Show this help message
|
|
185
|
-
|
|
186
|
-
Create/Init Options:
|
|
187
|
-
--typescript Create TypeScript project
|
|
188
|
-
--minimal Create minimal project structure
|
|
189
|
-
|
|
190
|
-
Compile Options:
|
|
191
|
-
--watch, -w Watch files and recompile on changes
|
|
192
|
-
--dry-run Show what would be compiled without writing
|
|
193
|
-
--output, -o Output directory (default: same as input)
|
|
194
|
-
|
|
195
|
-
Lint Options:
|
|
196
|
-
--fix Auto-fix fixable issues
|
|
197
|
-
--watch, -w Watch files and re-lint on changes
|
|
198
|
-
--dry-run Show fixes without applying (use with --fix)
|
|
199
|
-
|
|
200
|
-
Format Options:
|
|
201
|
-
--check Check formatting without writing (dry-run)
|
|
202
|
-
--watch, -w Watch files and re-format on changes
|
|
203
|
-
--write Write formatted output (default)
|
|
204
|
-
|
|
205
|
-
Analyze Options:
|
|
206
|
-
--json Output analysis as JSON
|
|
207
|
-
--verbose Show detailed metrics
|
|
208
|
-
|
|
209
|
-
Test Options:
|
|
210
|
-
--coverage, -c Collect code coverage
|
|
211
|
-
--watch, -w Watch files and re-run tests
|
|
212
|
-
--filter, -f Filter tests by name pattern
|
|
213
|
-
--timeout, -t Test timeout in ms (default: 30000)
|
|
214
|
-
--bail, -b Stop on first failure
|
|
215
|
-
--create <name> Generate a new test file
|
|
216
|
-
|
|
217
|
-
Doctor Options:
|
|
218
|
-
--verbose, -v Show detailed diagnostics
|
|
219
|
-
--json Output as JSON
|
|
220
|
-
|
|
221
|
-
Scaffold Options:
|
|
222
|
-
--dir, -d <path> Output directory
|
|
223
|
-
--force, -f Overwrite existing files
|
|
224
|
-
--props Include props section (components)
|
|
225
|
-
|
|
226
|
-
Docs Options:
|
|
227
|
-
--generate, -g Generate documentation
|
|
228
|
-
--format, -f Output format: markdown, json, html
|
|
229
|
-
--output, -o Output directory (default: docs/api)
|
|
230
|
-
|
|
231
|
-
Release Options:
|
|
232
|
-
--dry-run Show what would be done without making changes
|
|
233
|
-
--no-push Create commit and tag but don't push
|
|
234
|
-
--title <text> Release title for changelog
|
|
235
|
-
--skip-prompt Use empty changelog (for automation)
|
|
236
|
-
--skip-docs-test Skip documentation tests before release
|
|
237
|
-
--from-commits Auto-extract changelog from git commits since last tag
|
|
238
|
-
|
|
239
|
-
Examples:
|
|
240
|
-
pulse create my-app
|
|
241
|
-
pulse create my-app --typescript
|
|
242
|
-
pulse init --typescript
|
|
243
|
-
pulse dev
|
|
244
|
-
pulse build
|
|
245
|
-
pulse test
|
|
246
|
-
pulse test --coverage --watch
|
|
247
|
-
pulse test --create MyComponent
|
|
248
|
-
pulse doctor
|
|
249
|
-
pulse doctor --verbose
|
|
250
|
-
pulse scaffold component Button
|
|
251
|
-
pulse scaffold page Dashboard
|
|
252
|
-
pulse scaffold store user
|
|
253
|
-
pulse docs --generate
|
|
254
|
-
pulse docs --generate --format html
|
|
255
|
-
pulse compile src/App.pulse
|
|
256
|
-
pulse lint src/ --fix
|
|
257
|
-
pulse format --check
|
|
258
|
-
pulse analyze --json
|
|
259
|
-
pulse release patch
|
|
260
|
-
|
|
261
|
-
Documentation: https://github.com/vincenthirtz/pulse-js-framework
|
|
262
|
-
`);
|
|
183
|
+
function showHelp(args = []) {
|
|
184
|
+
runHelp(args);
|
|
263
185
|
}
|
|
264
186
|
|
|
265
187
|
/**
|
|
@@ -764,6 +686,80 @@ async function runScaffoldCmd(args) {
|
|
|
764
686
|
await runScaffold(args);
|
|
765
687
|
}
|
|
766
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Create a new .pulse file
|
|
691
|
+
* Usage: pulse new <name> [options]
|
|
692
|
+
* Options:
|
|
693
|
+
* --type, -t <type> Type: component, page, layout (default: component)
|
|
694
|
+
* --dir, -d <path> Output directory (default: src/components or based on type)
|
|
695
|
+
* --force, -f Overwrite existing files
|
|
696
|
+
* --props Include props section
|
|
697
|
+
* --no-state Skip state section
|
|
698
|
+
* --no-style Skip style section
|
|
699
|
+
*/
|
|
700
|
+
async function newPulseFile(args) {
|
|
701
|
+
const { options, patterns } = parseArgs(args);
|
|
702
|
+
const name = patterns[0];
|
|
703
|
+
|
|
704
|
+
if (!name) {
|
|
705
|
+
log.error('Please provide a name for the .pulse file.');
|
|
706
|
+
log.info(`
|
|
707
|
+
Usage: pulse new <name> [options]
|
|
708
|
+
|
|
709
|
+
Options:
|
|
710
|
+
--type, -t <type> Type: component, page, layout (default: component)
|
|
711
|
+
--dir, -d <path> Output directory
|
|
712
|
+
--force, -f Overwrite existing files
|
|
713
|
+
--props Include props section
|
|
714
|
+
--no-state Skip state section
|
|
715
|
+
--no-style Skip style section
|
|
716
|
+
|
|
717
|
+
Examples:
|
|
718
|
+
pulse new Button Create src/components/Button.pulse
|
|
719
|
+
pulse new Dashboard --type page Create src/pages/Dashboard.pulse
|
|
720
|
+
pulse new Admin --type layout Create src/layouts/Admin.pulse
|
|
721
|
+
pulse new Modal --props Create component with props section
|
|
722
|
+
pulse new Card -d src/ui Create in custom directory
|
|
723
|
+
`);
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Determine type
|
|
728
|
+
const type = options.type || options.t || 'component';
|
|
729
|
+
const validTypes = ['component', 'page', 'layout'];
|
|
730
|
+
|
|
731
|
+
if (!validTypes.includes(type)) {
|
|
732
|
+
log.error(`Invalid type: ${type}`);
|
|
733
|
+
log.info(`Valid types: ${validTypes.join(', ')}`);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Map type to scaffold type and delegate
|
|
738
|
+
const { runScaffold } = await import('./scaffold.js');
|
|
739
|
+
|
|
740
|
+
// Build args for scaffold command
|
|
741
|
+
const scaffoldArgs = [type, name];
|
|
742
|
+
|
|
743
|
+
// Pass through options
|
|
744
|
+
if (options.dir || options.d) {
|
|
745
|
+
scaffoldArgs.push('--dir', options.dir || options.d);
|
|
746
|
+
}
|
|
747
|
+
if (options.force || options.f) {
|
|
748
|
+
scaffoldArgs.push('--force');
|
|
749
|
+
}
|
|
750
|
+
if (options.props) {
|
|
751
|
+
scaffoldArgs.push('--props');
|
|
752
|
+
}
|
|
753
|
+
if (options.state === false) {
|
|
754
|
+
scaffoldArgs.push('--no-state');
|
|
755
|
+
}
|
|
756
|
+
if (options.style === false) {
|
|
757
|
+
scaffoldArgs.push('--no-style');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
await runScaffold(scaffoldArgs);
|
|
761
|
+
}
|
|
762
|
+
|
|
767
763
|
/**
|
|
768
764
|
* Run docs command
|
|
769
765
|
*/
|
package/cli/utils/file-utils.js
CHANGED
|
@@ -247,6 +247,10 @@ export function resolveImportPath(fromFile, importPath) {
|
|
|
247
247
|
|
|
248
248
|
/**
|
|
249
249
|
* Parse CLI arguments into options and file patterns
|
|
250
|
+
* Supports:
|
|
251
|
+
* - Boolean flags: --verbose, -v
|
|
252
|
+
* - Value options: --dir /path, -d /path
|
|
253
|
+
* - Negation: --no-state
|
|
250
254
|
* @param {string[]} args - Command line arguments
|
|
251
255
|
* @returns {{ options: object, patterns: string[] }}
|
|
252
256
|
*/
|
|
@@ -254,15 +258,33 @@ export function parseArgs(args) {
|
|
|
254
258
|
const options = {};
|
|
255
259
|
const patterns = [];
|
|
256
260
|
|
|
261
|
+
// Options that take a value (not boolean)
|
|
262
|
+
const valueOptions = new Set([
|
|
263
|
+
'dir', 'd', 'output', 'o', 'type', 't', 'format', 'f', 'filter', 'timeout', 'title', 'from'
|
|
264
|
+
]);
|
|
265
|
+
|
|
257
266
|
for (let i = 0; i < args.length; i++) {
|
|
258
267
|
const arg = args[i];
|
|
259
|
-
if (arg.startsWith('--')) {
|
|
268
|
+
if (arg.startsWith('--no-')) {
|
|
269
|
+
// Negation: --no-state -> state: false
|
|
270
|
+
const key = arg.slice(5);
|
|
271
|
+
options[key] = false;
|
|
272
|
+
} else if (arg.startsWith('--')) {
|
|
260
273
|
const key = arg.slice(2);
|
|
261
|
-
//
|
|
262
|
-
|
|
274
|
+
// Check if this option takes a value
|
|
275
|
+
if (valueOptions.has(key) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
276
|
+
options[key] = args[++i];
|
|
277
|
+
} else {
|
|
278
|
+
options[key] = true;
|
|
279
|
+
}
|
|
263
280
|
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
264
281
|
const key = arg.slice(1);
|
|
265
|
-
|
|
282
|
+
// Check if this option takes a value
|
|
283
|
+
if (valueOptions.has(key) && i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
284
|
+
options[key] = args[++i];
|
|
285
|
+
} else {
|
|
286
|
+
options[key] = true;
|
|
287
|
+
}
|
|
266
288
|
} else {
|
|
267
289
|
patterns.push(arg);
|
|
268
290
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.17",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
"LICENSE"
|
|
110
110
|
],
|
|
111
111
|
"scripts": {
|
|
112
|
-
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-adapter && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:docs-nav && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:a11y-enhanced && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress",
|
|
112
|
+
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-element && npm run test:dom-adapter && npm run test:enhanced-mock-adapter && npm run test:router && npm run test:store && npm run test:context && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:cli && npm run test:cli-ui && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:docs-nav && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:a11y-enhanced && npm run test:logger && npm run test:errors && npm run test:security && npm run test:websocket && npm run test:graphql && npm run test:doctor && npm run test:scaffold && npm run test:test-runner && npm run test:build && npm run test:integration && npm run test:context-stress && npm run test:form-edge-cases && npm run test:graphql-subscriptions && npm run test:http-edge-cases && npm run test:integration-advanced && npm run test:websocket-stress && npm run test:ssr",
|
|
113
113
|
"test:compiler": "node test/compiler.test.js",
|
|
114
114
|
"test:sourcemap": "node test/sourcemap.test.js",
|
|
115
115
|
"test:pulse": "node test/pulse.test.js",
|
|
@@ -153,6 +153,7 @@
|
|
|
153
153
|
"test:http-edge-cases": "node test/http-edge-cases.test.js",
|
|
154
154
|
"test:integration-advanced": "node test/integration-advanced.test.js",
|
|
155
155
|
"test:websocket-stress": "node test/websocket-stress.test.js",
|
|
156
|
+
"test:ssr": "node test/ssr.test.js",
|
|
156
157
|
"build:netlify": "node scripts/build-netlify.js",
|
|
157
158
|
"version": "node scripts/sync-version.js",
|
|
158
159
|
"docs": "node cli/index.js dev docs"
|
package/runtime/async.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { pulse, effect, batch, onCleanup } from './pulse.js';
|
|
10
|
+
import { getSSRAsyncContext, registerAsync, getCachedAsync, hasCachedAsync } from './ssr-async.js';
|
|
10
11
|
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// Versioned Async - Centralized Race Condition Handling
|
|
@@ -328,6 +329,44 @@ export function useAsync(asyncFn, options = {}) {
|
|
|
328
329
|
retryDelay = 1000
|
|
329
330
|
} = options;
|
|
330
331
|
|
|
332
|
+
// SSR MODE: Check for cached data or register async operation
|
|
333
|
+
const ssrCtx = getSSRAsyncContext();
|
|
334
|
+
if (ssrCtx) {
|
|
335
|
+
// Check if we already have cached data (second render pass)
|
|
336
|
+
if (hasCachedAsync(asyncFn)) {
|
|
337
|
+
const cachedData = getCachedAsync(asyncFn);
|
|
338
|
+
return {
|
|
339
|
+
data: pulse(cachedData),
|
|
340
|
+
error: pulse(null),
|
|
341
|
+
loading: pulse(false),
|
|
342
|
+
status: pulse('success'),
|
|
343
|
+
execute: () => Promise.resolve(cachedData),
|
|
344
|
+
reset: () => {},
|
|
345
|
+
abort: () => {}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// First render pass: register async operation for collection
|
|
350
|
+
if (immediate) {
|
|
351
|
+
const promise = asyncFn().catch(err => {
|
|
352
|
+
// Store error for SSR error handling
|
|
353
|
+
return null;
|
|
354
|
+
});
|
|
355
|
+
registerAsync(asyncFn, promise);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Return loading state for first pass
|
|
359
|
+
return {
|
|
360
|
+
data: pulse(initialData),
|
|
361
|
+
error: pulse(null),
|
|
362
|
+
loading: pulse(true),
|
|
363
|
+
status: pulse('loading'),
|
|
364
|
+
execute: () => Promise.resolve(initialData),
|
|
365
|
+
reset: () => {},
|
|
366
|
+
abort: () => {}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
331
370
|
const data = pulse(initialData);
|
|
332
371
|
const error = pulse(null);
|
|
333
372
|
const loading = pulse(false);
|
package/runtime/dom-element.js
CHANGED
|
@@ -10,6 +10,16 @@ import { loggers } from './logger.js';
|
|
|
10
10
|
import { safeSetAttribute } from './utils.js';
|
|
11
11
|
import { getAdapter } from './dom-adapter.js';
|
|
12
12
|
import { parseSelector } from './dom-selector.js';
|
|
13
|
+
import {
|
|
14
|
+
isHydratingMode,
|
|
15
|
+
getHydrationContext,
|
|
16
|
+
getCurrentNode,
|
|
17
|
+
advanceCursor,
|
|
18
|
+
enterChild,
|
|
19
|
+
exitChild,
|
|
20
|
+
registerListener,
|
|
21
|
+
warnMismatch
|
|
22
|
+
} from './ssr-hydrator.js';
|
|
13
23
|
|
|
14
24
|
const log = loggers.dom;
|
|
15
25
|
|
|
@@ -214,6 +224,62 @@ function applyRoleRequirements(element, role, attrs, dom) {
|
|
|
214
224
|
export function el(selector, ...children) {
|
|
215
225
|
const dom = getAdapter();
|
|
216
226
|
const config = parseSelector(selector);
|
|
227
|
+
|
|
228
|
+
// HYDRATION MODE: Reuse existing DOM element
|
|
229
|
+
if (isHydratingMode()) {
|
|
230
|
+
const ctx = getHydrationContext();
|
|
231
|
+
const existing = getCurrentNode(ctx);
|
|
232
|
+
|
|
233
|
+
// Verify element matches
|
|
234
|
+
if (existing && existing.nodeType === 1) {
|
|
235
|
+
const tag = existing.tagName?.toLowerCase();
|
|
236
|
+
if (tag !== config.tag) {
|
|
237
|
+
warnMismatch(ctx, `<${config.tag}>`, existing);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Process children to attach event handlers from attributes
|
|
241
|
+
const [attrs, childContent] = separateAttrsAndChildren(children);
|
|
242
|
+
|
|
243
|
+
if (attrs) {
|
|
244
|
+
// Attach event handlers to existing element
|
|
245
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
246
|
+
if (key.startsWith('on') && typeof value === 'function') {
|
|
247
|
+
const event = key.slice(2).toLowerCase();
|
|
248
|
+
registerListener(ctx, existing, event, value);
|
|
249
|
+
}
|
|
250
|
+
// Handle reactive attributes
|
|
251
|
+
else if (typeof value === 'function' && !key.startsWith('on')) {
|
|
252
|
+
effect(() => {
|
|
253
|
+
const result = value();
|
|
254
|
+
if (key === 'class' || key === 'className') {
|
|
255
|
+
existing.className = result || '';
|
|
256
|
+
} else if (key === 'style' && typeof result === 'string') {
|
|
257
|
+
existing.style.cssText = result;
|
|
258
|
+
} else if (result != null) {
|
|
259
|
+
existing.setAttribute(key, result);
|
|
260
|
+
} else {
|
|
261
|
+
existing.removeAttribute(key);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Enter child scope and process children
|
|
269
|
+
enterChild(ctx, existing);
|
|
270
|
+
for (const child of childContent) {
|
|
271
|
+
hydrateChild(existing, child, ctx);
|
|
272
|
+
}
|
|
273
|
+
exitChild(ctx, existing);
|
|
274
|
+
|
|
275
|
+
return existing;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// No matching element found, warn and fall through to create
|
|
279
|
+
warnMismatch(ctx, `<${config.tag}>`, existing);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// NORMAL MODE: Create new element
|
|
217
283
|
const element = dom.createElement(config.tag);
|
|
218
284
|
|
|
219
285
|
if (config.id) {
|
|
@@ -240,6 +306,47 @@ export function el(selector, ...children) {
|
|
|
240
306
|
return element;
|
|
241
307
|
}
|
|
242
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Separate attributes object from children in el() arguments
|
|
311
|
+
* @private
|
|
312
|
+
*/
|
|
313
|
+
function separateAttrsAndChildren(children) {
|
|
314
|
+
if (children.length === 0) {
|
|
315
|
+
return [null, []];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const first = children[0];
|
|
319
|
+
if (first && typeof first === 'object' && !Array.isArray(first) &&
|
|
320
|
+
!(first instanceof Node) && !(first.nodeType)) {
|
|
321
|
+
return [first, children.slice(1)];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return [null, children];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Hydrate a child element (attach listeners without creating DOM)
|
|
329
|
+
* @private
|
|
330
|
+
*/
|
|
331
|
+
function hydrateChild(parent, child, ctx) {
|
|
332
|
+
if (child == null || child === false) return;
|
|
333
|
+
|
|
334
|
+
if (typeof child === 'string' || typeof child === 'number') {
|
|
335
|
+
// Text content - just advance cursor
|
|
336
|
+
advanceCursor(ctx);
|
|
337
|
+
} else if (typeof child === 'function') {
|
|
338
|
+
// Reactive child - set up effect but skip initial DOM creation
|
|
339
|
+
effect(() => {
|
|
340
|
+
child(); // Execute to track dependencies, but don't modify DOM on first run in hydration
|
|
341
|
+
});
|
|
342
|
+
} else if (Array.isArray(child)) {
|
|
343
|
+
for (const c of child) {
|
|
344
|
+
hydrateChild(parent, c, ctx);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Node children are handled by recursive el() calls
|
|
348
|
+
}
|
|
349
|
+
|
|
243
350
|
/**
|
|
244
351
|
* Append a child to an element, handling various types
|
|
245
352
|
*
|
package/runtime/index.js
CHANGED
|
@@ -1,25 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pulse Runtime - Main exports
|
|
3
|
+
*
|
|
4
|
+
* Core modules are re-exported here for convenience.
|
|
5
|
+
* Development-only modules (devtools, hmr) should be imported directly
|
|
6
|
+
* from their respective paths to enable tree-shaking in production.
|
|
3
7
|
*/
|
|
4
8
|
|
|
9
|
+
// Core reactivity
|
|
5
10
|
export * from './pulse.js';
|
|
11
|
+
|
|
12
|
+
// DOM creation and manipulation
|
|
6
13
|
export * from './dom.js';
|
|
14
|
+
|
|
15
|
+
// Routing
|
|
7
16
|
export * from './router.js';
|
|
17
|
+
|
|
18
|
+
// State management
|
|
8
19
|
export * from './store.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export * from './a11y.js';
|
|
20
|
+
|
|
21
|
+
// Context API
|
|
12
22
|
export * from './context.js';
|
|
23
|
+
|
|
24
|
+
// Async primitives (useAsync, useResource, usePolling)
|
|
25
|
+
export * from './async.js';
|
|
26
|
+
|
|
27
|
+
// Form handling (useForm, useField, validators)
|
|
28
|
+
export * from './form.js';
|
|
29
|
+
|
|
30
|
+
// HTTP client
|
|
31
|
+
export * from './http.js';
|
|
32
|
+
|
|
33
|
+
// WebSocket client
|
|
13
34
|
export * from './websocket.js';
|
|
35
|
+
|
|
36
|
+
// GraphQL client
|
|
14
37
|
export * from './graphql.js';
|
|
15
38
|
|
|
39
|
+
// Server-side rendering
|
|
40
|
+
export * from './ssr.js';
|
|
41
|
+
|
|
42
|
+
// Accessibility utilities
|
|
43
|
+
export * from './a11y.js';
|
|
44
|
+
|
|
45
|
+
// Native mobile bridge
|
|
46
|
+
export * from './native.js';
|
|
47
|
+
|
|
48
|
+
// Logging
|
|
49
|
+
export * from './logger.js';
|
|
50
|
+
|
|
51
|
+
// Security utilities (XSS prevention, URL sanitization)
|
|
52
|
+
export * from './utils.js';
|
|
53
|
+
|
|
54
|
+
// Error classes
|
|
55
|
+
export * from './errors.js';
|
|
56
|
+
|
|
57
|
+
// LRU Cache
|
|
58
|
+
export * from './lru-cache.js';
|
|
59
|
+
|
|
60
|
+
// DOM Adapter (for SSR/testing)
|
|
61
|
+
export * from './dom-adapter.js';
|
|
62
|
+
|
|
63
|
+
// Default exports for namespace imports
|
|
16
64
|
export { default as PulseCore } from './pulse.js';
|
|
17
65
|
export { default as PulseDOM } from './dom.js';
|
|
18
66
|
export { default as PulseRouter } from './router.js';
|
|
19
67
|
export { default as PulseStore } from './store.js';
|
|
20
|
-
export { default as PulseNative } from './native.js';
|
|
21
|
-
export { default as PulseLogger } from './logger.js';
|
|
22
|
-
export { default as PulseA11y } from './a11y.js';
|
|
23
68
|
export { default as PulseContext } from './context.js';
|
|
69
|
+
export { default as PulseAsync } from './async.js';
|
|
70
|
+
export { default as PulseForm } from './form.js';
|
|
71
|
+
export { default as PulseHttp } from './http.js';
|
|
24
72
|
export { default as PulseWebSocket } from './websocket.js';
|
|
25
73
|
export { default as PulseGraphQL } from './graphql.js';
|
|
74
|
+
export { default as PulseSSR } from './ssr.js';
|
|
75
|
+
export { default as PulseA11y } from './a11y.js';
|
|
76
|
+
export { default as PulseNative } from './native.js';
|
|
77
|
+
export { default as PulseLogger } from './logger.js';
|
|
78
|
+
|
|
79
|
+
// Note: The following modules are intentionally NOT re-exported here
|
|
80
|
+
// to enable tree-shaking. Import them directly when needed:
|
|
81
|
+
//
|
|
82
|
+
// Development tools (adds overhead, use only in dev):
|
|
83
|
+
// import { enableDevTools, trackedPulse } from 'pulse-js-framework/runtime/devtools';
|
|
84
|
+
//
|
|
85
|
+
// HMR utilities (Vite/webpack integration):
|
|
86
|
+
// import { createHMRContext } from 'pulse-js-framework/runtime/hmr';
|
|
87
|
+
//
|
|
88
|
+
// Lite build (minimal ~5KB bundle):
|
|
89
|
+
// import { pulse, effect, el, mount } from 'pulse-js-framework/runtime/lite';
|
package/runtime/pulse.js
CHANGED
|
@@ -23,6 +23,35 @@ import { Errors } from './errors.js';
|
|
|
23
23
|
|
|
24
24
|
const log = loggers.pulse;
|
|
25
25
|
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// SSR MODE FLAG
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* SSR mode flag - when true, effects run once without setting up subscriptions.
|
|
32
|
+
* This is set by the SSR module during server-side rendering.
|
|
33
|
+
* @type {boolean}
|
|
34
|
+
*/
|
|
35
|
+
let ssrModeEnabled = false;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if SSR mode is enabled.
|
|
39
|
+
* In SSR mode, effects run once but don't subscribe to changes.
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function isSSRMode() {
|
|
43
|
+
return ssrModeEnabled;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Set SSR mode (used internally by ssr.js).
|
|
48
|
+
* @param {boolean} enabled - Whether to enable SSR mode
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export function setSSRMode(enabled) {
|
|
52
|
+
ssrModeEnabled = enabled;
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
// =============================================================================
|
|
27
56
|
// REACTIVE DEPENDENCY TRACKING ALGORITHM
|
|
28
57
|
// =============================================================================
|
|
@@ -885,6 +914,17 @@ export function effect(fn, options = {}) {
|
|
|
885
914
|
const { id: customId, onError } = options;
|
|
886
915
|
const effectId = customId || `effect_${++effectIdCounter}`;
|
|
887
916
|
|
|
917
|
+
// SSR MODE: Run effect once without subscriptions
|
|
918
|
+
if (ssrModeEnabled) {
|
|
919
|
+
try {
|
|
920
|
+
fn();
|
|
921
|
+
} catch (e) {
|
|
922
|
+
log.warn(`SSR effect error (${effectId}):`, e.message);
|
|
923
|
+
}
|
|
924
|
+
// Return noop cleanup function
|
|
925
|
+
return () => {};
|
|
926
|
+
}
|
|
927
|
+
|
|
888
928
|
// Capture module ID at creation time for HMR tracking
|
|
889
929
|
const moduleId = activeContext.currentModuleId;
|
|
890
930
|
|