pulse-js-framework 1.7.25 → 1.7.26

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 CHANGED
@@ -48,6 +48,27 @@ npm install
48
48
  npm run dev
49
49
  ```
50
50
 
51
+ ### Or from a template
52
+
53
+ Create projects from built-in example apps:
54
+
55
+ ```bash
56
+ # E-commerce app (products, cart, checkout)
57
+ npx pulse-js-framework create my-shop --ecommerce
58
+
59
+ # Todo app (filtering, local storage)
60
+ npx pulse-js-framework create my-todos --todo
61
+
62
+ # Blog (posts, sidebar, navigation)
63
+ npx pulse-js-framework create my-blog --blog
64
+
65
+ # Chat app (messages, users, emoji picker)
66
+ npx pulse-js-framework create my-chat --chat
67
+
68
+ # Dashboard (data visualization)
69
+ npx pulse-js-framework create my-dashboard --dashboard
70
+ ```
71
+
51
72
  ### Or use directly
52
73
 
53
74
  ```javascript
@@ -139,6 +160,11 @@ See [Pulse DSL documentation](docs/pulse-dsl.md) for full syntax reference.
139
160
  # Project Creation
140
161
  pulse create <name> # Create new project
141
162
  pulse create <name> --typescript # Create TypeScript project
163
+ pulse create <name> --ecommerce # Create from E-Commerce template
164
+ pulse create <name> --todo # Create from Todo App template
165
+ pulse create <name> --blog # Create from Blog template
166
+ pulse create <name> --chat # Create from Chat template
167
+ pulse create <name> --dashboard # Create from Dashboard template
142
168
  pulse init --typescript # Initialize in current directory
143
169
 
144
170
  # Development
package/cli/help.js CHANGED
@@ -29,18 +29,29 @@ Creates a new Pulse project with a complete starter template including:
29
29
  - Project structure (src/, public/)
30
30
  - Vite configuration for development and building
31
31
  - Sample App.pulse component with counter example
32
- - Package.json with all necessary scripts`,
32
+ - Package.json with all necessary scripts
33
+
34
+ You can also create projects from built-in example templates using
35
+ the template flags (--ecommerce, --todo, --blog, --chat, --dashboard).`,
33
36
  arguments: [
34
37
  { name: '<name>', description: 'Name of the project directory to create' }
35
38
  ],
36
39
  options: [
37
40
  { flag: '--typescript, --ts', description: 'Create a TypeScript project with tsconfig.json' },
38
- { flag: '--minimal', description: 'Create minimal project structure without extras' }
41
+ { flag: '--minimal', description: 'Create minimal project structure without extras' },
42
+ { flag: '--ecommerce', description: 'Use E-Commerce template (products, cart, checkout)' },
43
+ { flag: '--todo', description: 'Use Todo App template (filtering, local storage)' },
44
+ { flag: '--blog', description: 'Use Blog template (posts, sidebar, navigation)' },
45
+ { flag: '--chat', description: 'Use Chat template (messages, users, emoji picker)' },
46
+ { flag: '--dashboard', description: 'Use Dashboard template (data visualization)' }
39
47
  ],
40
48
  examples: [
41
49
  { cmd: 'pulse create my-app', desc: 'Create a new JavaScript project' },
42
50
  { cmd: 'pulse create my-app --typescript', desc: 'Create a new TypeScript project' },
43
- { cmd: 'pulse create my-app --minimal', desc: 'Create a minimal project' }
51
+ { cmd: 'pulse create my-app --minimal', desc: 'Create a minimal project' },
52
+ { cmd: 'pulse create my-shop --ecommerce', desc: 'Create from E-Commerce template' },
53
+ { cmd: 'pulse create my-todos --todo', desc: 'Create from Todo App template' },
54
+ { cmd: 'pulse create my-blog --blog', desc: 'Create from Blog template' }
44
55
  ]
45
56
  },
46
57
 
package/cli/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { fileURLToPath } from 'url';
8
8
  import { dirname, join, resolve, relative } from 'path';
9
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, watch } from 'fs';
9
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, watch, cpSync, statSync } from 'fs';
10
10
  import { log } from './logger.js';
11
11
  import { findPulseFiles, parseArgs } from './utils/file-utils.js';
12
12
  import { runHelp } from './help.js';
@@ -18,6 +18,15 @@ const __dirname = dirname(__filename);
18
18
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
19
19
  const VERSION = pkg.version;
20
20
 
21
+ // Available example templates
22
+ const TEMPLATES = {
23
+ ecommerce: { name: 'E-Commerce', description: 'Shopping cart with products, checkout, and cart management' },
24
+ todo: { name: 'Todo App', description: 'Classic todo list with filtering and local storage' },
25
+ blog: { name: 'Blog', description: 'Blogging platform with posts, sidebar, and navigation' },
26
+ chat: { name: 'Chat', description: 'Real-time chat with messages, users, and emoji picker' },
27
+ dashboard: { name: 'Dashboard', description: 'Analytics dashboard with data visualization' }
28
+ };
29
+
21
30
  // Command handlers
22
31
  const commands = {
23
32
  help: showHelp,
@@ -191,6 +200,87 @@ function showVersion() {
191
200
  log.info(`Pulse Framework v${VERSION}`);
192
201
  }
193
202
 
203
+ /**
204
+ * Copy example template to project directory
205
+ * Transforms imports from /runtime/index.js to pulse-js-framework/runtime
206
+ * @param {string} templateName - Name of the template (ecommerce, todo, blog, chat, dashboard)
207
+ * @param {string} projectPath - Destination project path
208
+ * @param {string} projectName - Name of the project
209
+ */
210
+ function copyExampleTemplate(templateName, projectPath, projectName) {
211
+ const examplesDir = join(__dirname, '..', 'examples');
212
+ const templateDir = join(examplesDir, templateName);
213
+
214
+ if (!existsSync(templateDir)) {
215
+ throw new Error(`Template "${templateName}" not found at ${templateDir}`);
216
+ }
217
+
218
+ /**
219
+ * Recursively copy directory, transforming JS files
220
+ */
221
+ function copyDir(src, dest) {
222
+ if (!existsSync(dest)) {
223
+ mkdirSync(dest, { recursive: true });
224
+ }
225
+
226
+ const entries = readdirSync(src, { withFileTypes: true });
227
+
228
+ for (const entry of entries) {
229
+ const srcPath = join(src, entry.name);
230
+ const destPath = join(dest, entry.name);
231
+
232
+ // Skip node_modules and dist directories
233
+ if (entry.name === 'node_modules' || entry.name === 'dist') {
234
+ continue;
235
+ }
236
+
237
+ if (entry.isDirectory()) {
238
+ copyDir(srcPath, destPath);
239
+ } else {
240
+ // Read file content
241
+ let content = readFileSync(srcPath, 'utf-8');
242
+
243
+ // Transform imports in JS files
244
+ if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
245
+ // Transform /runtime/index.js imports to pulse-js-framework/runtime
246
+ content = content.replace(
247
+ /from\s+['"]\/runtime\/index\.js['"]/g,
248
+ "from 'pulse-js-framework/runtime'"
249
+ );
250
+ content = content.replace(
251
+ /from\s+['"]\/runtime['"]/g,
252
+ "from 'pulse-js-framework/runtime'"
253
+ );
254
+ // Transform other runtime submodule imports
255
+ content = content.replace(
256
+ /from\s+['"]\/runtime\/([^'"]+)['"]/g,
257
+ "from 'pulse-js-framework/runtime/$1'"
258
+ );
259
+ }
260
+
261
+ writeFileSync(destPath, content);
262
+ }
263
+ }
264
+ }
265
+
266
+ // Copy src directory
267
+ const srcDir = join(templateDir, 'src');
268
+ if (existsSync(srcDir)) {
269
+ copyDir(srcDir, join(projectPath, 'src'));
270
+ }
271
+
272
+ // Copy index.html if exists
273
+ const indexHtml = join(templateDir, 'index.html');
274
+ if (existsSync(indexHtml)) {
275
+ let content = readFileSync(indexHtml, 'utf-8');
276
+ // Update title to project name
277
+ content = content.replace(/<title>[^<]*<\/title>/, `<title>${projectName}</title>`);
278
+ writeFileSync(join(projectPath, 'index.html'), content);
279
+ }
280
+
281
+ return true;
282
+ }
283
+
194
284
  /**
195
285
  * Create a new project
196
286
  */
@@ -200,7 +290,16 @@ async function createProject(args) {
200
290
 
201
291
  if (!projectName) {
202
292
  log.error('Please provide a project name.');
203
- log.info('Usage: pulse create <project-name>');
293
+ log.info('Usage: pulse create <project-name> [options]');
294
+ log.info('');
295
+ log.info('Options:');
296
+ log.info(' --typescript, --ts Create TypeScript project');
297
+ log.info(' --minimal Create minimal project');
298
+ log.info('');
299
+ log.info('Templates (use existing example apps):');
300
+ for (const [key, info] of Object.entries(TEMPLATES)) {
301
+ log.info(` --${key.padEnd(12)} ${info.description}`);
302
+ }
204
303
  process.exit(1);
205
304
  }
206
305
 
@@ -214,6 +313,93 @@ async function createProject(args) {
214
313
  const useTypescript = options.typescript || options.ts || false;
215
314
  const minimal = options.minimal || false;
216
315
 
316
+ // Check for template options
317
+ let selectedTemplate = null;
318
+ for (const templateName of Object.keys(TEMPLATES)) {
319
+ if (options[templateName]) {
320
+ selectedTemplate = templateName;
321
+ break;
322
+ }
323
+ }
324
+
325
+ // If a template is selected, use the template-based creation
326
+ if (selectedTemplate) {
327
+ const templateInfo = TEMPLATES[selectedTemplate];
328
+ log.info(`Creating new Pulse project: ${projectName} (${templateInfo.name} template)`);
329
+
330
+ // Create project directory
331
+ mkdirSync(projectPath);
332
+ mkdirSync(join(projectPath, 'public'));
333
+
334
+ // Copy template files
335
+ try {
336
+ copyExampleTemplate(selectedTemplate, projectPath, projectName);
337
+ log.info(` ✓ Copied ${templateInfo.name} template files`);
338
+ } catch (err) {
339
+ log.error(`Failed to copy template: ${err.message}`);
340
+ process.exit(1);
341
+ }
342
+
343
+ // Create package.json for template project
344
+ const packageJson = {
345
+ name: projectName,
346
+ version: '0.1.0',
347
+ type: 'module',
348
+ scripts: {
349
+ dev: 'pulse dev',
350
+ build: 'pulse build',
351
+ preview: 'vite preview',
352
+ test: 'pulse test',
353
+ lint: 'pulse lint'
354
+ },
355
+ dependencies: {
356
+ 'pulse-js-framework': '^1.0.0'
357
+ },
358
+ devDependencies: {
359
+ vite: '^5.0.0'
360
+ }
361
+ };
362
+
363
+ writeFileSync(
364
+ join(projectPath, 'package.json'),
365
+ JSON.stringify(packageJson, null, 2)
366
+ );
367
+
368
+ // Create vite.config.js
369
+ const viteConfig = `import { defineConfig } from 'vite';
370
+ import pulse from 'pulse-js-framework/vite';
371
+
372
+ export default defineConfig({
373
+ plugins: [pulse()]
374
+ });
375
+ `;
376
+ writeFileSync(join(projectPath, 'vite.config.js'), viteConfig);
377
+
378
+ // Create .gitignore
379
+ const gitignore = `node_modules
380
+ dist
381
+ .DS_Store
382
+ *.local
383
+ `;
384
+ writeFileSync(join(projectPath, '.gitignore'), gitignore);
385
+
386
+ log.info(`
387
+ Project created successfully!
388
+
389
+ Next steps:
390
+ cd ${projectName}
391
+ npm install
392
+ npm run dev
393
+
394
+ Template: ${templateInfo.name}
395
+ ${templateInfo.description}
396
+
397
+ Happy coding with Pulse!
398
+ `);
399
+ return;
400
+ }
401
+
402
+ // Standard project creation (no template)
217
403
  log.info(`Creating new Pulse project: ${projectName}${useTypescript ? ' (TypeScript)' : ''}`);
218
404
 
219
405
  // Create project structure
@@ -355,8 +355,17 @@ function parseBalancedExpression(str, start) {
355
355
  * @param {string} selector - CSS selector with potential dynamic attributes
356
356
  * @returns {Object} { cleanSelector, dynamicAttrs }
357
357
  */
358
+ /**
359
+ * Extract all attributes (static and dynamic) from a selector string.
360
+ * Static attributes become part of the attrs object passed to el().
361
+ * Dynamic attributes (with {expr} values) are bound via bind().
362
+ *
363
+ * @param {string} selector - Element selector like "div.class[href=url][value={expr}]"
364
+ * @returns {{cleanSelector: string, staticAttrs: Array<{name: string, value: string}>, dynamicAttrs: Array<{name: string, expr: string}>}}
365
+ */
358
366
  function extractDynamicAttributes(selector) {
359
367
  const dynamicAttrs = [];
368
+ const staticAttrs = [];
360
369
  let cleanSelector = '';
361
370
  let i = 0;
362
371
 
@@ -367,7 +376,7 @@ function extractDynamicAttributes(selector) {
367
376
 
368
377
  // Parse attribute name
369
378
  let attrName = '';
370
- while (i < selector.length && /[a-zA-Z0-9-]/.test(selector[i])) {
379
+ while (i < selector.length && /[a-zA-Z0-9-_]/.test(selector[i])) {
371
380
  attrName += selector[i];
372
381
  i++;
373
382
  }
@@ -382,9 +391,12 @@ function extractDynamicAttributes(selector) {
382
391
  // Skip whitespace
383
392
  while (i < selector.length && /\s/.test(selector[i])) i++;
384
393
 
385
- // Check for optional quote
386
- const hasQuote = selector[i] === '"';
387
- if (hasQuote) i++;
394
+ // Determine quote character (or none)
395
+ let quoteChar = null;
396
+ if (selector[i] === '"' || selector[i] === "'") {
397
+ quoteChar = selector[i];
398
+ i++;
399
+ }
388
400
 
389
401
  // Check for dynamic expression {
390
402
  if (selector[i] === '{') {
@@ -394,7 +406,7 @@ function extractDynamicAttributes(selector) {
394
406
  i = result.end + 1; // Skip past closing }
395
407
 
396
408
  // Skip optional closing quote
397
- if (hasQuote && selector[i] === '"') i++;
409
+ if (quoteChar && selector[i] === quoteChar) i++;
398
410
 
399
411
  // Skip closing ]
400
412
  if (selector[i] === ']') i++;
@@ -403,16 +415,40 @@ function extractDynamicAttributes(selector) {
403
415
  continue;
404
416
  }
405
417
  }
406
- }
407
418
 
408
- // Not a dynamic attribute, copy everything from [ to ]
409
- let bracketDepth = 1;
410
- cleanSelector += '[';
411
- while (i < selector.length && bracketDepth > 0) {
412
- if (selector[i] === '[') bracketDepth++;
413
- else if (selector[i] === ']') bracketDepth--;
414
- cleanSelector += selector[i];
415
- i++;
419
+ // Static attribute - parse the value
420
+ let attrValue = '';
421
+ if (quoteChar) {
422
+ // Quoted value - read until closing quote
423
+ while (i < selector.length && selector[i] !== quoteChar) {
424
+ attrValue += selector[i];
425
+ i++;
426
+ }
427
+ // Skip closing quote
428
+ if (selector[i] === quoteChar) i++;
429
+ } else {
430
+ // Unquoted value - read until ]
431
+ while (i < selector.length && selector[i] !== ']') {
432
+ attrValue += selector[i];
433
+ i++;
434
+ }
435
+ }
436
+
437
+ // Skip closing ]
438
+ if (selector[i] === ']') i++;
439
+
440
+ // Add to static attrs (don't put in selector)
441
+ staticAttrs.push({ name: attrName, value: attrValue });
442
+ continue;
443
+ } else {
444
+ // Boolean attribute (no value) like [disabled]
445
+ // Skip to closing ]
446
+ while (i < selector.length && selector[i] !== ']') i++;
447
+ if (selector[i] === ']') i++;
448
+
449
+ // Add as boolean attribute
450
+ staticAttrs.push({ name: attrName, value: '' });
451
+ continue;
416
452
  }
417
453
  } else {
418
454
  cleanSelector += selector[i];
@@ -420,7 +456,7 @@ function extractDynamicAttributes(selector) {
420
456
  }
421
457
  }
422
458
 
423
- return { cleanSelector, dynamicAttrs };
459
+ return { cleanSelector, staticAttrs, dynamicAttrs };
424
460
  }
425
461
 
426
462
  /**
@@ -445,8 +481,8 @@ export function transformElement(transformer, node, indent) {
445
481
  return transformComponentCall(transformer, node, indent);
446
482
  }
447
483
 
448
- // Extract dynamic attributes from selector (e.g., [value={searchQuery}])
449
- let { cleanSelector, dynamicAttrs } = extractDynamicAttributes(node.selector);
484
+ // Extract all attributes from selector (static and dynamic)
485
+ let { cleanSelector, staticAttrs, dynamicAttrs } = extractDynamicAttributes(node.selector);
450
486
 
451
487
  // Add scoped class to selector if CSS scoping is enabled
452
488
  let selector = cleanSelector;
@@ -483,51 +519,46 @@ export function transformElement(transformer, node, indent) {
483
519
  transformer.usesA11y.trapFocus = true;
484
520
  }
485
521
 
486
- // Build ARIA attributes from directives
487
- const ariaAttrs = [];
522
+ // Collect all static attributes for the el() attrs object
523
+ const allStaticAttrs = [];
488
524
 
489
- // Process @a11y directives
525
+ // Add attributes extracted from selector
526
+ for (const attr of staticAttrs) {
527
+ // Escape single quotes in values
528
+ const escapedValue = attr.value.replace(/'/g, "\\'");
529
+ if (attr.value === '') {
530
+ // Boolean attribute
531
+ allStaticAttrs.push(`'${attr.name}': true`);
532
+ } else {
533
+ allStaticAttrs.push(`'${attr.name}': '${escapedValue}'`);
534
+ }
535
+ }
536
+
537
+ // Process @a11y directives - add to static attrs
490
538
  for (const directive of a11yDirectives) {
491
539
  const attrs = buildA11yAttributes(transformer, directive);
492
540
  for (const [key, value] of Object.entries(attrs)) {
493
541
  const valueCode = typeof value === 'string' ? `'${value}'` : value;
494
- ariaAttrs.push(`'${key}': ${valueCode}`);
542
+ allStaticAttrs.push(`'${key}': ${valueCode}`);
495
543
  }
496
544
  }
497
545
 
498
546
  // Process @live directives (add aria-live and aria-atomic)
499
547
  for (const directive of liveDirectives) {
500
548
  const priority = directive.priority || 'polite';
501
- ariaAttrs.push(`'aria-live': '${priority}'`);
502
- ariaAttrs.push(`'aria-atomic': 'true'`);
503
- }
504
-
505
- // Build selector with inline ARIA attributes
506
- let enhancedSelector = selector;
507
- if (ariaAttrs.length > 0) {
508
- // Convert ARIA attrs to selector attribute syntax where possible
509
- // For dynamic values, we'll need to use setAriaAttributes
510
- const staticAttrs = [];
511
- const dynamicAttrs = [];
512
-
513
- for (const attr of ariaAttrs) {
514
- const match = attr.match(/^'([^']+)':\s*'([^']+)'$/);
515
- if (match) {
516
- // Static attribute - can embed in selector
517
- staticAttrs.push(`[${match[1]}=${match[2]}]`);
518
- } else {
519
- dynamicAttrs.push(attr);
520
- }
521
- }
522
-
523
- // Add static ARIA attributes to selector
524
- enhancedSelector = selector + staticAttrs.join('');
549
+ allStaticAttrs.push(`'aria-live': '${priority}'`);
550
+ allStaticAttrs.push(`'aria-atomic': 'true'`);
525
551
  }
526
552
 
527
553
  // Start with el() call - escape single quotes in selector
528
- const escapedSelector = enhancedSelector.replace(/'/g, "\\'");
554
+ const escapedSelector = selector.replace(/'/g, "\\'");
529
555
  parts.push(`${pad}el('${escapedSelector}'`);
530
556
 
557
+ // Add attributes object if we have any static attributes
558
+ if (allStaticAttrs.length > 0) {
559
+ parts.push(`, { ${allStaticAttrs.join(', ')} }`);
560
+ }
561
+
531
562
  // Add text content
532
563
  if (node.textContent.length > 0) {
533
564
  for (const text of node.textContent) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulse-js-framework",
3
- "version": "1.7.25",
3
+ "version": "1.7.26",
4
4
  "description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
5
5
  "type": "module",
6
6
  "main": "index.js",