fusee-framework 1.6.0 → 1.7.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.
Files changed (112) hide show
  1. package/README.md +29 -13
  2. package/app/benchmark/benchmark.js +633 -0
  3. package/app/benchmark/index.html +102 -0
  4. package/app/benchmark/styles.css +249 -0
  5. package/app/components/Counter.js +17 -7
  6. package/app/components/CounterSection.js +3 -4
  7. package/app/main.js +29 -62
  8. package/app/main.ts +72 -63
  9. package/app/pages/404.js +16 -0
  10. package/app/pages/[...all].js +35 -0
  11. package/app/pages/[...all].ts +35 -0
  12. package/app/pages/_layout.js +26 -0
  13. package/app/pages/_layout.ts +26 -0
  14. package/app/pages/about.js +23 -0
  15. package/app/pages/about.ts +29 -0
  16. package/app/pages/composables.js +158 -0
  17. package/app/pages/composables.ts +158 -0
  18. package/app/pages/dynamic.js +40 -0
  19. package/app/pages/dynamic.ts +40 -0
  20. package/app/pages/index.js +117 -0
  21. package/app/pages/index.ts +78 -0
  22. package/app/pages/login.ts +21 -0
  23. package/app/pages/objects/[id].js +56 -0
  24. package/app/pages/objects/[id].ts +56 -0
  25. package/app/pages/posts/[category]/[slug].js +56 -0
  26. package/app/pages/posts/[category]/[slug].ts +56 -0
  27. package/app/pages/router-example.js +56 -0
  28. package/app/pages/router-example.ts +56 -0
  29. package/app/pages/test-for.js +65 -0
  30. package/app/pages/test-for.ts +80 -0
  31. package/app/pages/users/[id].js +24 -0
  32. package/app/pages/users/[id].ts +46 -0
  33. package/app/pages/users/index.js +22 -0
  34. package/app/pages/users/index.ts +29 -0
  35. package/app/pages/users.js +43 -0
  36. package/app/pages/users.ts +78 -0
  37. package/app/routes/About.js +1 -1
  38. package/app/routes/About.ts +1 -1
  39. package/app/routes/AuthRoutes.ts +41 -0
  40. package/app/routes/Home.js +9 -0
  41. package/app/routes/NotFound.js +16 -0
  42. package/app/routes/NotFound.ts +16 -0
  43. package/app/routes/RouterExample.js +56 -0
  44. package/app/routes/RouterExample.ts +56 -0
  45. package/app/routes/WildcardDemo.js +35 -0
  46. package/app/routes/WildcardDemo.ts +35 -0
  47. package/auto-imports.d.ts +1 -0
  48. package/bin/create-fusee-linux +0 -0
  49. package/bin/create-fusee-mac-arm64 +0 -0
  50. package/bin/create-fusee-mac-intel +0 -0
  51. package/bin/create-fusee-win.exe +0 -0
  52. package/bin/create-fusee.exe +0 -0
  53. package/bin/index.js +38 -0
  54. package/create-fusee-go/cmd/generate.go +72 -0
  55. package/create-fusee-go/cmd/init.go +38 -0
  56. package/create-fusee-go/cmd/root.go +121 -0
  57. package/create-fusee-go/go.mod +10 -0
  58. package/create-fusee-go/go.sum +10 -0
  59. package/create-fusee-go/internal/assets/assets.go +78 -0
  60. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
  61. package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
  62. package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
  63. package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
  64. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
  65. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
  66. package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
  67. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
  68. package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
  69. package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
  70. package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
  71. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
  72. package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
  73. package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
  74. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
  75. package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
  76. package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
  77. package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
  78. package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
  79. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
  80. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
  81. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
  82. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
  83. package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
  84. package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
  85. package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
  86. package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
  87. package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
  88. package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
  89. package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
  90. package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
  91. package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
  92. package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
  93. package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
  94. package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
  95. package/create-fusee-go/main.go +7 -0
  96. package/framework/auto-import-preset.js +1 -1
  97. package/framework/core/compiler.js +252 -249
  98. package/framework/core/component.js +265 -260
  99. package/framework/core/directives.js +875 -786
  100. package/framework/core/evaluator.js +153 -70
  101. package/framework/core/event-delegation.js +33 -14
  102. package/framework/core/signal.js +412 -403
  103. package/framework/index.js +10 -11
  104. package/framework/router/file-router.js +177 -0
  105. package/framework/router/router.js +462 -80
  106. package/framework/types/event-delegation.d.ts +0 -58
  107. package/framework/types/router.d.ts +32 -15
  108. package/framework/types/signal.d.ts +1 -0
  109. package/index.html +638 -626
  110. package/package.json +5 -3
  111. package/tsconfig.json +1 -0
  112. /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
Binary file
Binary file
package/bin/index.js ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import fs from 'fs';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ function getBinaryName() {
10
+ const platform = process.platform;
11
+ const arch = process.arch;
12
+
13
+ if (platform === 'win32') return 'create-fusee-win.exe';
14
+ if (platform === 'darwin') {
15
+ return arch === 'arm64' ? 'create-fusee-mac-arm64' : 'create-fusee-mac-intel';
16
+ }
17
+ if (platform === 'linux') return 'create-fusee-linux';
18
+
19
+ return 'create-fusee-linux'; // Default fallback
20
+ }
21
+
22
+ const binName = getBinaryName();
23
+ const binPath = path.join(__dirname, binName);
24
+
25
+ // Fallback for local development if specific named binary isn't there
26
+ const finalPath = fs.existsSync(binPath) ? binPath : path.join(__dirname, 'create-fusee.exe');
27
+
28
+ if (!fs.existsSync(finalPath)) {
29
+ console.error(`\n❌ Error: Fusée binary not found for your platform (${process.platform})`);
30
+ process.exit(1)
31
+ }
32
+
33
+ const args = process.argv.slice(2);
34
+ const child = spawn(finalPath, args, { stdio: 'inherit' });
35
+
36
+ child.on('exit', (code) => {
37
+ process.exit(code);
38
+ });
@@ -0,0 +1,72 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "path/filepath"
7
+ "strings"
8
+
9
+ "create-fusee/internal/assets"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ var genCmd = &cobra.Command{
14
+ Use: "generate [type] [name]",
15
+ Aliases: []string{"g"},
16
+ Short: "Generate a new page or component",
17
+ Long: `Generate a new Fusée resource.
18
+ Types available: page (p), component (c)`,
19
+ Args: cobra.ExactArgs(2),
20
+ Run: func(cmd *cobra.Command, args []string) {
21
+ genType := strings.ToLower(args[0])
22
+ name := strings.Title(args[1])
23
+
24
+ // Check if we are in a Fusée project
25
+ if _, err := os.Stat("framework"); os.IsNotExist(err) {
26
+ fmt.Println("❌ Error: This command must be run from the root of a Fusée project.")
27
+ os.Exit(1)
28
+ }
29
+
30
+ ext := "js"
31
+ if _, err := os.Stat("tsconfig.json"); err == nil {
32
+ ext = "ts"
33
+ }
34
+
35
+ config := assets.Config{
36
+ ProjectName: name,
37
+ IsTS: ext == "ts",
38
+ Ext: ext,
39
+ }
40
+
41
+ var dest string
42
+ var tmpl string
43
+
44
+ switch genType {
45
+ case "page", "p":
46
+ dest = filepath.Join("app/pages", args[1]+"."+ext)
47
+ tmpl = "templates/page.tmpl"
48
+ case "component", "c":
49
+ dest = filepath.Join("app/components", args[1]+"."+ext)
50
+ tmpl = "templates/component.tmpl"
51
+ default:
52
+ fmt.Printf("❌ Error: Unknown type '%s'. Use 'page' or 'component'.\n", genType)
53
+ os.Exit(1)
54
+ }
55
+
56
+ if _, err := os.Stat(dest); err == nil {
57
+ fmt.Printf("❌ Error: File '%s' already exists.\n", dest)
58
+ os.Exit(1)
59
+ }
60
+
61
+ if err := assets.WriteTemplate(tmpl, dest, config); err != nil {
62
+ fmt.Printf("❌ Error: Could not generate %s: %v\n", genType, err)
63
+ os.Exit(1)
64
+ }
65
+
66
+ fmt.Printf("✅ %s created: %s\n", strings.Title(genType), dest)
67
+ },
68
+ }
69
+
70
+ func init() {
71
+ rootCmd.AddCommand(genCmd)
72
+ }
@@ -0,0 +1,38 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bufio"
5
+ "fmt"
6
+ "os"
7
+ "strings"
8
+
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+
13
+ var initCmd = &cobra.Command{
14
+ Use: "init [project-name]",
15
+ Short: "Initialize a new Fusée project",
16
+ Args: cobra.MaximumNArgs(1),
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ projectName := "my-fusee-app"
19
+ if len(args) > 0 {
20
+ projectName = args[0]
21
+ }
22
+
23
+ isTS := IsTSFlag
24
+ if !cmd.Flags().Changed("ts") {
25
+ reader := bufio.NewReader(os.Stdin)
26
+ fmt.Printf("🧬 Select language template [JavaScript (js) / TypeScript (ts)] (default: js): ")
27
+ langInput, _ := reader.ReadString('\n')
28
+ isTS = strings.TrimSpace(strings.ToLower(langInput)) == "ts"
29
+ }
30
+
31
+ runInitWithParams(projectName, isTS)
32
+ },
33
+ }
34
+
35
+ func init() {
36
+ initCmd.Flags().BoolVarP(&IsTSFlag, "ts", "t", false, "Use TypeScript template")
37
+ rootCmd.AddCommand(initCmd)
38
+ }
@@ -0,0 +1,121 @@
1
+ package cmd
2
+
3
+ import (
4
+ "bufio"
5
+ "fmt"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+
10
+ "create-fusee/internal/assets"
11
+ "github.com/spf13/cobra"
12
+ )
13
+
14
+ var IsTSFlag bool
15
+
16
+ var rootCmd = &cobra.Command{
17
+ Use: "fusee",
18
+ Short: "Fusée is a high-performance reactive framework CLI",
19
+ Long: `Fusée CLI helps you scaffold new projects, generate components,
20
+ and manage your Fusée application development workflow.`,
21
+ Args: cobra.MaximumNArgs(1),
22
+ Run: func(cmd *cobra.Command, args []string) {
23
+ if len(args) > 0 {
24
+ isTS := IsTSFlag
25
+ if !cmd.Flags().Changed("ts") {
26
+ reader := bufio.NewReader(os.Stdin)
27
+ fmt.Printf("🧬 Select language template [JavaScript (js) / TypeScript (ts)] (default: js): ")
28
+ langInput, _ := reader.ReadString('\n')
29
+ isTS = strings.TrimSpace(strings.ToLower(langInput)) == "ts"
30
+ }
31
+ runInitWithParams(args[0], isTS)
32
+ } else {
33
+ cmd.Help()
34
+ }
35
+ },
36
+ }
37
+
38
+ func Execute() {
39
+ if err := rootCmd.Execute(); err != nil {
40
+ fmt.Println(err)
41
+ os.Exit(1)
42
+ }
43
+ }
44
+
45
+ func init() {
46
+ rootCmd.Flags().BoolVarP(&IsTSFlag, "ts", "t", false, "Use TypeScript template")
47
+ }
48
+
49
+ func runInitWithParams(projectName string, isTS bool) {
50
+ if strings.ContainsAny(projectName, " !@#$%^&*()") {
51
+ fmt.Printf("❌ Error: Project name '%s' contains invalid characters.\n", projectName)
52
+ os.Exit(1)
53
+ }
54
+
55
+ projectPath, err := filepath.Abs(projectName)
56
+ if err != nil {
57
+ fmt.Printf("❌ Error: Could not determine absolute path: %v\n", err)
58
+ os.Exit(1)
59
+ }
60
+
61
+ if _, err := os.Stat(projectPath); !os.IsNotExist(err) && projectName != "." {
62
+ fmt.Printf("❌ Error: Directory '%s' already exists.\n", projectName)
63
+ os.Exit(1)
64
+ }
65
+
66
+ ext := "js"
67
+ if isTS {
68
+ ext = "ts"
69
+ }
70
+
71
+ fmt.Printf("\n🚀 Scaffolding a new project in: %s...\n", projectPath)
72
+
73
+ config := assets.Config{
74
+ ProjectName: filepath.Base(projectName),
75
+ IsTS: isTS,
76
+ Ext: ext,
77
+ BtnStyle: "background:#ff3333; color:white; border:none; width:50px; height:50px; border-radius:15px; cursor:pointer; font-size:1.5rem; transition: 0.2s;",
78
+ }
79
+ if projectName == "." {
80
+ config.ProjectName = "fusee-project"
81
+ }
82
+
83
+ dirs := []string{"app/components", "app/pages", "app/routes", "framework"}
84
+ for _, d := range dirs {
85
+ if err := os.MkdirAll(filepath.Join(projectPath, d), 0755); err != nil {
86
+ fmt.Printf("❌ Error: Could not create directory %s: %v\n", d, err)
87
+ os.Exit(1)
88
+ }
89
+ }
90
+
91
+ fmt.Println("📦 Injecting Fusée Core Engine...")
92
+ if err := assets.CopyEmbeddedDir("embed/framework", filepath.Join(projectPath, "framework"), isTS); err != nil {
93
+ fmt.Printf("❌ Error: Could not inject framework: %v\n", err)
94
+ os.Exit(1)
95
+ }
96
+
97
+ files := map[string]string{
98
+ "templates/package.json.tmpl": "package.json",
99
+ "templates/vite.config.js.tmpl": "vite.config.js",
100
+ "templates/index.html.tmpl": "index.html",
101
+ "templates/main.tmpl": "app/main." + ext,
102
+ "templates/components/Loading.tmpl": "app/components/Loading." + ext,
103
+ "templates/pages/layout.tmpl": "app/pages/_layout." + ext,
104
+ "templates/pages/index.tmpl": "app/pages/index." + ext,
105
+ "templates/pages/about.tmpl": "app/pages/about." + ext,
106
+ "templates/Counter.tmpl": "app/components/Counter." + ext,
107
+ }
108
+ if isTS {
109
+ files["templates/tsconfig.json.tmpl"] = "tsconfig.json"
110
+ }
111
+
112
+ for src, dest := range files {
113
+ if err := assets.WriteTemplate(src, filepath.Join(projectPath, dest), config); err != nil {
114
+ fmt.Printf("❌ Error: Could not write file %s: %v\n", dest, err)
115
+ os.Exit(1)
116
+ }
117
+ }
118
+
119
+ fmt.Println("\n✅ Fusée Project Ready!")
120
+ fmt.Printf("👉 Run: cd %s && npm install && npm run dev\n\n", projectName)
121
+ }
@@ -0,0 +1,10 @@
1
+ module create-fusee
2
+
3
+ go 1.21
4
+
5
+ require github.com/spf13/cobra v1.8.0
6
+
7
+ require (
8
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
9
+ github.com/spf13/pflag v1.0.5 // indirect
10
+ )
@@ -0,0 +1,10 @@
1
+ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2
+ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3
+ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
4
+ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5
+ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
6
+ github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
7
+ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
8
+ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
9
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -0,0 +1,78 @@
1
+ package assets
2
+
3
+ import (
4
+ "embed"
5
+ "io/fs"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+ "text/template"
10
+ )
11
+
12
+ //go:embed all:templates all:embed/framework
13
+ var EmbeddedFiles embed.FS
14
+
15
+ type Config struct {
16
+ ProjectName string
17
+ IsTS bool
18
+ Ext string
19
+ BtnStyle string
20
+ }
21
+
22
+ func CopyEmbeddedDir(srcDir, destDir string, keepTypes bool) error {
23
+ return fs.WalkDir(EmbeddedFiles, srcDir, func(path string, d fs.DirEntry, err error) error {
24
+ if err != nil {
25
+ return err
26
+ }
27
+
28
+ relPath := strings.TrimPrefix(path, srcDir)
29
+ relPath = strings.TrimPrefix(relPath, "/")
30
+
31
+ if relPath == "" {
32
+ return nil
33
+ }
34
+
35
+ if !keepTypes && (strings.HasSuffix(relPath, ".d.ts") || strings.HasPrefix(relPath, "types")) {
36
+ return nil
37
+ }
38
+
39
+ parts := strings.Split(relPath, "/")
40
+ for _, part := range parts {
41
+ if part == "bin" || part == "node_modules" || part == ".git" || part == "dist" || part == "__tests__" {
42
+ return nil
43
+ }
44
+ }
45
+
46
+ destPath := filepath.Join(destDir, filepath.FromSlash(relPath))
47
+
48
+ if d.IsDir() {
49
+ return os.MkdirAll(destPath, 0755)
50
+ }
51
+
52
+ data, err := EmbeddedFiles.ReadFile(path)
53
+ if err != nil {
54
+ return err
55
+ }
56
+ return os.WriteFile(destPath, data, 0644)
57
+ })
58
+ }
59
+
60
+ func WriteTemplate(tmplPath, destPath string, config Config) error {
61
+ data, err := EmbeddedFiles.ReadFile(tmplPath)
62
+ if err != nil {
63
+ return err
64
+ }
65
+
66
+ tmpl, err := template.New(tmplPath).Parse(string(data))
67
+ if err != nil {
68
+ return err
69
+ }
70
+
71
+ f, err := os.Create(destPath)
72
+ if err != nil {
73
+ return err
74
+ }
75
+ defer f.Close()
76
+
77
+ return tmpl.Execute(f, config)
78
+ }
@@ -0,0 +1,9 @@
1
+ export const FuseePreset = {
2
+ 'fusee-framework': [
3
+ 'signal', 'computed', 'effect', 'watch',
4
+ 'batch', 'untrack', 'inspect', 'onCleanup',
5
+ 'defineComponent', 'onMount', 'onUnmount',
6
+ 'directive',
7
+ 'mountOutlet', 'mountTemplate', 'emit'
8
+ ]
9
+ }
@@ -0,0 +1,253 @@
1
+ import { effect, batch } from './signal.js'
2
+ import { processDirectives } from './directives.js'
3
+ import { parseSlots } from './component.js'
4
+ import {
5
+ evaluateExpression,
6
+ parseInterpolation,
7
+ sanitizeAttr,
8
+ MUSTACHE_RE
9
+ } from './evaluator.js'
10
+
11
+ export function mountTemplate(template, container, context, components) {
12
+ const effects = []
13
+ let processed = template.replace(/(<[a-zA-Z0-9-]+\s[^>]*?)\s:([a-zA-Z][a-zA-Z0-9-]*)=/g, '$1 data-bind-$2=')
14
+
15
+ for (const name of Object.keys(components)) {
16
+ const reWithSlot = new RegExp(
17
+ `\\{\\{\\s*${name}(\\s[^}]*?|\\s*)\\}\\}([\\s\\S]*?)\\{\\{\\s*\\/${name}\\s*\\}\\}`,
18
+ 'g'
19
+ )
20
+ const reNoSlot = new RegExp(`\\{\\{\\s*${name}(\\s+.*?|\\s*)\\}\\}`, 'g')
21
+
22
+ processed = processed.replace(reWithSlot, (match, propsStr, slotContent) => {
23
+ const attrs = parseComponentAttrs(propsStr || '')
24
+ const encodedSlot = encodeURIComponent(slotContent.trim())
25
+ return `<div data-component="${name}"${attrs} data-slot="${encodedSlot}"></div>`
26
+ })
27
+
28
+ processed = processed.replace(reNoSlot, (match, propsStr) => {
29
+ const attrs = parseComponentAttrs(propsStr || '')
30
+ return `<div data-component="${name}"${attrs}></div>`
31
+ })
32
+ }
33
+
34
+ container.innerHTML = processed
35
+ compileNode(container, context, components, effects)
36
+
37
+ return { effects }
38
+ }
39
+
40
+ function parseComponentAttrs(propsStr) {
41
+ let attrs = ''
42
+ if (!propsStr || !propsStr.trim()) return attrs
43
+
44
+ const attrRegex = /(@?:?[a-zA-Z0-9-]+)="([^"]*)"/g
45
+ let attrMatch
46
+ while ((attrMatch = attrRegex.exec(propsStr)) !== null) {
47
+ const attrName = attrMatch[1]
48
+ const attrValue = attrMatch[2]
49
+
50
+ if (attrName.startsWith('@')) {
51
+ attrs += ` data-on-${attrName.slice(1).toLowerCase()}="${attrValue}"`
52
+ } else if (attrName.startsWith(':')) {
53
+ attrs += ` data-bind-prop-${attrName.slice(1).toLowerCase()}="${attrValue}"`
54
+ } else {
55
+ attrs += ` data-prop-${attrName.toLowerCase()}="${attrValue}"`
56
+ }
57
+ }
58
+ return attrs
59
+ }
60
+
61
+ export function compileNode(node, context, components, effects) {
62
+ if (node.nodeType === 1 && node.hasAttribute('f-once')) {
63
+ node.removeAttribute('f-once')
64
+ const temporaryEffects = []
65
+
66
+ processDirectives(node, context, components, temporaryEffects)
67
+ processBindingAttrs(node, context, temporaryEffects)
68
+ processMustaches(node, context, temporaryEffects)
69
+ bindComponents(node, components, context, temporaryEffects)
70
+
71
+ let child = node.firstChild
72
+ while (child) {
73
+ compileOnce(child, context, components, temporaryEffects)
74
+ child = child.nextSibling
75
+ }
76
+
77
+ for (let i = temporaryEffects.length - 1; i >= 0; i--) {
78
+ const cleanup = temporaryEffects[i]
79
+ if (typeof cleanup === 'function') cleanup()
80
+ }
81
+ return
82
+ }
83
+
84
+ if (node.nodeType === 1) {
85
+ if (processDirectives(node, context, components, effects)) return
86
+ processBindingAttrs(node, context, effects)
87
+ bindComponents(node, components, context, effects)
88
+ } else if (node.nodeType === 3) {
89
+ processMustaches(node, context, effects)
90
+ }
91
+
92
+ let child = node.firstChild
93
+ while (child) {
94
+ compileNode(child, context, components, effects)
95
+ child = child.nextSibling
96
+ }
97
+ }
98
+
99
+ function compileOnce(node, context, components, effects) {
100
+ if (node.nodeType === 1) {
101
+ if (processDirectives(node, context, components, effects)) return
102
+ processBindingAttrs(node, context, effects)
103
+ bindComponents(node, components, context, effects)
104
+ } else if (node.nodeType === 3) {
105
+ processMustaches(node, context, effects)
106
+ }
107
+
108
+ let child = node.firstChild
109
+ while (child) {
110
+ compileOnce(child, context, components, effects)
111
+ child = child.nextSibling
112
+ }
113
+ }
114
+
115
+ function processMustaches(node, context, effects) {
116
+ if (node.nodeType !== 3) return
117
+ const text = node.textContent
118
+ if (!MUSTACHE_RE.test(text)) return
119
+
120
+ const parts = parseInterpolation(text)
121
+ const fragment = document.createDocumentFragment()
122
+
123
+ for (const part of parts) {
124
+ if (part.type === 'static') {
125
+ fragment.appendChild(document.createTextNode(part.value))
126
+ } else {
127
+ const reactiveNode = document.createTextNode('')
128
+ fragment.appendChild(reactiveNode)
129
+ const key = part.key
130
+ const e = effect(() => {
131
+ const resolved = evaluateExpression(key, context)
132
+ reactiveNode.textContent = String(resolved ?? '')
133
+ })
134
+ effects.push(e)
135
+ }
136
+ }
137
+ node.parentNode.replaceChild(fragment, node)
138
+ }
139
+
140
+ function processBindingAttrs(el, context, effects) {
141
+ if (el.nodeType !== 1) return
142
+ for (const attr of [...el.attributes]) {
143
+ if (attr.name.startsWith('data-bind-') && !attr.name.startsWith('data-bind-prop-')) {
144
+ const realAttr = attr.name.slice(10)
145
+ const expr = attr.value
146
+ el.removeAttribute(attr.name)
147
+
148
+ const e = effect(() => {
149
+ let resolved = evaluateExpression(expr, context)
150
+ if (realAttr === 'class') {
151
+ if (!el._staticClass) el._staticClass = el.getAttribute('class') || ''
152
+ let dynamicClass = ''
153
+ if (Array.isArray(resolved)) dynamicClass = resolved.filter(Boolean).join(' ')
154
+ else if (resolved !== null && typeof resolved === 'object') {
155
+ dynamicClass = Object.entries(resolved).filter(([_, v]) => !!v).map(([k]) => k).join(' ')
156
+ } else dynamicClass = String(resolved ?? '')
157
+ el.className = (el._staticClass + ' ' + dynamicClass).trim()
158
+ } else if (realAttr === 'style') {
159
+ if (resolved !== null && typeof resolved === 'object') {
160
+ for (const k in resolved) el.style[k] = resolved[k]
161
+ } else el.style.cssText = String(resolved ?? '')
162
+ } else {
163
+ if (typeof resolved === 'boolean') {
164
+ resolved ? el.setAttribute(realAttr, '') : el.removeAttribute(realAttr)
165
+ } else el.setAttribute(realAttr, sanitizeAttr(realAttr, String(resolved)))
166
+ }
167
+ })
168
+ effects.push(e)
169
+ continue
170
+ }
171
+
172
+ if (MUSTACHE_RE.test(attr.value)) {
173
+ const attrName = attr.name
174
+ const parts = parseInterpolation(attr.value)
175
+ el.removeAttribute(attrName)
176
+ const e = effect(() => {
177
+ let result = ''
178
+ for (const part of parts) {
179
+ result += part.type === 'static' ? part.value : String(evaluateExpression(part.key, context) ?? '')
180
+ }
181
+ el.setAttribute(attrName, sanitizeAttr(attrName, result))
182
+ })
183
+ effects.push(e)
184
+ }
185
+ }
186
+ }
187
+
188
+ function bindComponents(el, components, context, effects) {
189
+ if (el.nodeType !== 1) return
190
+ const name = el.getAttribute('data-component')
191
+ if (!name) return
192
+
193
+ el.removeAttribute('data-component')
194
+
195
+ const ComponentFn = components[name]
196
+ if (!ComponentFn) return
197
+
198
+ const props = {}
199
+ const listeners = {}
200
+
201
+ for (const attr of [...el.attributes]) {
202
+ const attrName = attr.name.toLowerCase()
203
+
204
+ if (attrName.startsWith('data-prop-')) {
205
+ props[attrName.slice(10)] = attr.value
206
+
207
+ } else if (attrName.startsWith('data-bind-prop-')) {
208
+ const propName = attrName.slice(15)
209
+ const expression = attr.value.trim()
210
+
211
+ if (expression in context) {
212
+ Object.defineProperty(props, propName, {
213
+ get() {
214
+ const val = context[expression]
215
+ return typeof val === 'function' && val.isSignal ? val() : val
216
+ },
217
+ enumerable: true,
218
+ configurable: true
219
+ })
220
+ } else {
221
+ Object.defineProperty(props, propName, {
222
+ get() { return evaluateExpression(expression, context) },
223
+ enumerable: true,
224
+ configurable: true
225
+ })
226
+ }
227
+
228
+ } else if (attrName.startsWith('data-on-')) {
229
+ const eventName = attrName.slice(8)
230
+ const handlerExpr = attr.value.trim()
231
+
232
+ listeners[eventName] = (...args) => {
233
+ const handler = context[handlerExpr]
234
+ if (typeof handler === 'function') {
235
+ handler(...args)
236
+ } else {
237
+ evaluateExpression(handlerExpr, context)
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ const rawSlot = el.getAttribute('data-slot')
244
+ const slots = rawSlot ? parseSlots(decodeURIComponent(rawSlot)) : {}
245
+ el.removeAttribute('data-slot')
246
+
247
+ const childComponent = ComponentFn(props, { listeners, slots, parent: context._instance })
248
+ childComponent.render(el)
249
+
250
+ if (effects) {
251
+ effects.push(() => childComponent.unmount())
252
+ }
253
+ }