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/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
- const command = args[0] || 'help';
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](args.slice(1));
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
- log.info(`
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
  */
@@ -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
- // Boolean flags - don't consume next argument
262
- options[key] = true;
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
- options[key] = true;
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.15",
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);
@@ -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
- export * from './native.js';
10
- export * from './logger.js';
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