From 72848973346cafd4e0630b7c628886adbe4c3906 Mon Sep 17 00:00:00 2001 From: Mohamed Melouk <42706279+cobraprojects@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:50:03 +0300 Subject: [PATCH 1/3] fix scaffolding --- bun.lock | 249 +----------------- packages/cli/src/dev.ts | 28 ++ packages/cli/src/project.ts | 1 + packages/cli/src/project/registry.ts | 83 ++++-- packages/cli/src/project/scaffold.ts | 120 +++++++-- packages/cli/tests/broadcast-registry.test.ts | 3 +- packages/cli/tests/cli.test.ts | 137 +++++++++- packages/core/src/portable/holo.ts | 13 +- packages/db/src/model/ModelQueryBuilder.ts | 20 +- packages/db/src/model/defineModel.ts | 8 +- packages/db/tests/model-relations.test.ts | 48 +++- 11 files changed, 404 insertions(+), 306 deletions(-) diff --git a/bun.lock b/bun.lock index a25e4e7..fa880f3 100644 --- a/bun.lock +++ b/bun.lock @@ -38,6 +38,7 @@ "@holo-js/cache": "^0.1.4", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", + "@holo-js/core": "^0.1.4", "@holo-js/db": "workspace:*", "@holo-js/db-mysql": "^0.1.4", "@holo-js/db-postgres": "^0.1.4", @@ -116,7 +117,7 @@ "@holo-js/cache-db": "^0.1.4", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "workspace:*", + "@holo-js/core": "^0.1.4", "@holo-js/db": "workspace:*", "@holo-js/db-sqlite": "^0.1.4", "@holo-js/events": "^0.1.4", @@ -202,7 +203,7 @@ "@holo-js/cache-db": "^0.1.4", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "workspace:*", + "@holo-js/core": "^0.1.4", "@holo-js/db": "workspace:*", "@holo-js/db-sqlite": "^0.1.4", "@holo-js/events": "^0.1.4", @@ -237,78 +238,6 @@ "vitepress": "catalog:", }, }, - "apps/scaffold-smoke-next": { - "name": "apps-scaffold-smoke-next", - "dependencies": { - "@holo-js/adapter-next": "^0.1.4", - "@holo-js/auth": "^0.1.4", - "@holo-js/cli": "^0.1.4", - "@holo-js/config": "^0.1.4", - "@holo-js/core": "^0.1.4", - "@holo-js/db": "^0.1.4", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "esbuild": "^0.25.0", - "next": "^16.0.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - }, - "devDependencies": { - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "typescript": "^5.8.0", - }, - }, - "apps/scaffold-smoke-nuxt": { - "name": "apps-scaffold-smoke-nuxt", - "dependencies": { - "@holo-js/adapter-nuxt": "^0.1.4", - "@holo-js/auth": "^0.1.4", - "@holo-js/cli": "^0.1.4", - "@holo-js/config": "^0.1.4", - "@holo-js/core": "^0.1.4", - "@holo-js/db": "^0.1.4", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "esbuild": "^0.25.0", - "nuxt": "latest", - "vue": "^3.5.13", - "vue-router": "^5.0.4", - }, - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.8.0", - "vite": "^5.4.14", - "vue-tsc": "^2.2.0", - }, - }, - "apps/scaffold-smoke-sveltekit": { - "name": "apps-scaffold-smoke-sveltekit", - "dependencies": { - "@holo-js/adapter-sveltekit": "^0.1.4", - "@holo-js/auth": "^0.1.4", - "@holo-js/cli": "^0.1.4", - "@holo-js/config": "^0.1.4", - "@holo-js/core": "^0.1.4", - "@holo-js/db": "^0.1.4", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@sveltejs/adapter-node": "^5.0.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", - "esbuild": "^0.25.0", - "svelte": "^5.0.0", - "vite": "^5.0.0", - }, - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.8.0", - }, - }, "apps/svelte_test_app": { "name": "svelte_test_app", "dependencies": { @@ -2062,12 +1991,6 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - "apps-scaffold-smoke-next": ["apps-scaffold-smoke-next@workspace:apps/scaffold-smoke-next"], - - "apps-scaffold-smoke-nuxt": ["apps-scaffold-smoke-nuxt@workspace:apps/scaffold-smoke-nuxt"], - - "apps-scaffold-smoke-sveltekit": ["apps-scaffold-smoke-sveltekit@workspace:apps/scaffold-smoke-sveltekit"], - "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="], "archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="], @@ -3502,16 +3425,6 @@ "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "apps-scaffold-smoke-next/@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - - "apps-scaffold-smoke-next/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - - "apps-scaffold-smoke-next/react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], - - "apps-scaffold-smoke-nuxt/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - - "apps-scaffold-smoke-sveltekit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "archiver/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], "archiver/tar-stream": ["tar-stream@3.1.8", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ=="], @@ -3734,162 +3647,6 @@ "@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], - "apps-scaffold-smoke-next/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "apps-scaffold-smoke-next/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "apps-scaffold-smoke-nuxt/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "apps-scaffold-smoke-sveltekit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "archiver-utils/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "archiver/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], diff --git a/packages/cli/src/dev.ts b/packages/cli/src/dev.ts index 212c621..2f6fb3c 100644 --- a/packages/cli/src/dev.ts +++ b/packages/cli/src/dev.ts @@ -8,6 +8,8 @@ import { ensureProjectConfig, syncManagedDriverDependencies, prepareProjectDiscovery, + renderFrameworkRunner, + writeTextFile, } from './project' import { hasProjectDependency } from './package-json' import type { @@ -137,6 +139,7 @@ export async function runProjectDependencyInstall( export async function runProjectPrepare(projectRoot: string, io?: IoStreams): Promise { const project = await ensureProjectConfig(projectRoot) await prepareProjectDiscovery(projectRoot, project.config) + await refreshFrameworkRunner(projectRoot) await runNuxtPrepare(projectRoot) await runSvelteKitSync(projectRoot) @@ -145,11 +148,36 @@ export async function runProjectPrepare(projectRoot: string, io?: IoStreams): Pr if (updatedDependencies && io) { await runProjectDependencyInstall(io, projectRoot) await prepareProjectDiscovery(projectRoot, project.config) + await refreshFrameworkRunner(projectRoot) await runNuxtPrepare(projectRoot) await runSvelteKitSync(projectRoot) } } +async function refreshFrameworkRunner(projectRoot: string): Promise { + const frameworkProjectPath = resolve(projectRoot, '.holo-js/framework/project.json') + const frameworkRunnerPath = resolve(projectRoot, '.holo-js/framework/run.mjs') + + try { + const content = await readFile(frameworkProjectPath, 'utf8') + const manifest = JSON.parse(content) as { framework?: unknown } + + if ( + manifest.framework !== 'next' + && manifest.framework !== 'nuxt' + && manifest.framework !== 'sveltekit' + ) { + return + } + + await writeTextFile(frameworkRunnerPath, renderFrameworkRunner({ + framework: manifest.framework, + })) + } catch { + return + } +} + async function runNuxtPrepare(projectRoot: string): Promise { const frameworkProjectPath = resolve(projectRoot, '.holo-js/framework/project.json') try { diff --git a/packages/cli/src/project.ts b/packages/cli/src/project.ts index a897e2a..0461302 100644 --- a/packages/cli/src/project.ts +++ b/packages/cli/src/project.ts @@ -187,6 +187,7 @@ export { makeProjectRelativePath, prepareProjectDiscovery, readTextFile, + renderFrameworkRunner, resolveDefaultArtifactPath, resolveGeneratedSchemaPath, resolveProjectPackageImportSpecifier, diff --git a/packages/cli/src/project/registry.ts b/packages/cli/src/project/registry.ts index 5545d3f..ba1044a 100644 --- a/packages/cli/src/project/registry.ts +++ b/packages/cli/src/project/registry.ts @@ -261,32 +261,58 @@ const SVELTE_HOOKS_OVERRIDE_BLOCK = [ ' },', ].join('\n') -function svelteConfigHasHooksOverride(contents: string): boolean { +function getSvelteConfigHooksOverrideState(contents: string): 'managed' | 'custom' | 'none' { if (contents.includes('.holo-js/generated/hooks')) { - return true + return 'managed' } - // Detect any existing kit.files.hooks override so we don't double-patch - // configs that already redirect hook entrypoints. - return /kit\s*:\s*\{[\s\S]*?files\s*:\s*\{[\s\S]*?hooks\s*:/.test(contents) + if (/kit\s*:\s*\{[\s\S]*?files\s*:\s*\{[\s\S]*?hooks\s*:/.test(contents)) { + return 'custom' + } + + return 'none' } function patchSvelteConfigWithHooksOverride(contents: string): string | undefined { - if (svelteConfigHasHooksOverride(contents)) { + const hooksOverrideState = getSvelteConfigHooksOverrideState(contents) + if (hooksOverrideState === 'managed') { return undefined } + if (hooksOverrideState === 'custom') { + throw new Error('Custom SvelteKit hook entrypoints are not supported. Remove kit.files.hooks from svelte.config.js and let holo prepare manage the generated hook bridge.') + } + + const singleLineKitPattern = /(kit:\s*\{)([^\n{}]*?)(\s*\},?)/m + const singleLineKitMatch = contents.match(singleLineKitPattern) + if (singleLineKitMatch) { + const opening = singleLineKitMatch[1] ?? 'kit: {' + const body = singleLineKitMatch[2] ?? '' + const closing = singleLineKitMatch[3] ?? '}' + const trimmedBody = body.trim() + const suffix = closing.trimStart().startsWith('},') ? ',' : '' + const bodyLines = trimmedBody + ? trimmedBody + .split(',') + .map(segment => segment.trim()) + .filter(Boolean) + .map(segment => ` ${segment},`) + : [] + + const replacement = [ + opening, + ...bodyLines, + SVELTE_HOOKS_OVERRIDE_BLOCK, + ` }${suffix}`, + ].join('\n') + + return contents.replace(singleLineKitPattern, replacement) + } // Try to inject the files.hooks block right after `kit: {` (with possible trailing content on the same line). - // Handle both multi-line `kit: {\n...` and single-line `kit: {}` or files without trailing newline. + // Handle multi-line `kit: {\n...` configs and files without trailing newline. const patched = contents.replace( /(kit:\s*\{)([^\n]*\n?)/, (match, opening: string, rest: string) => { - const trimmedRest = rest.trimEnd() - // Single-line `kit: {}` — inject before the closing brace - if (trimmedRest === '}' || trimmedRest === '},') { - const suffix = trimmedRest.endsWith(',') ? ',' : '' - return `${opening}\n${SVELTE_HOOKS_OVERRIDE_BLOCK}\n }${suffix}\n` - } return `${opening}${rest}${SVELTE_HOOKS_OVERRIDE_BLOCK}\n` }, ) @@ -327,13 +353,13 @@ async function ensureSvelteManagedHooks(projectRoot: string): Promise { // and delete the legacy files. if (legacyUserContents && (!hooksContents || isManagedPrepareArtifact(hooksContents))) { await writeFileIfChanged(hooksPath, legacyUserContents) + await unlinkIfPresent(legacyHooksUserPath) } - await unlinkIfPresent(legacyHooksUserPath) if (legacyServerUserContents && (!hooksServerContents || isManagedPrepareArtifact(hooksServerContents))) { await writeFileIfChanged(hooksServerPath, legacyServerUserContents) + await unlinkIfPresent(legacyHooksServerUserPath) } - await unlinkIfPresent(legacyHooksServerUserPath) // If the current src/hooks.ts is a legacy Holo-managed artifact or doesn't exist, // replace it with a clean default so the user owns the file. @@ -458,10 +484,9 @@ export function renderGeneratedBroadcastManifest( const hasPresenceChannels = registry.channels.some(entry => entry.type === 'presence') const manifestImports = hasPresenceChannels ? [ - 'import type { GeneratedBroadcastManifest } from \'@holo-js/core\'', 'import type { ChannelPresenceMemberFor } from \'@holo-js/broadcast\'', ] - : ['import type { GeneratedBroadcastManifest } from \'@holo-js/core\''] + : [] const eventLines = registry.broadcast.flatMap((entry, index) => { const lines = [ ' {', @@ -503,6 +528,30 @@ export function renderGeneratedBroadcastManifest( '// Generated by holo prepare. Do not edit.', '', ...manifestImports, + ...(manifestImports.length > 0 ? [''] : []), + 'type GeneratedBroadcastManifestEvent = {', + ' readonly name: string', + ' readonly channels: readonly {', + ' readonly type: \'public\' | \'private\' | \'presence\'', + ' readonly pattern: string', + ' }[]', + '}', + '', + 'type GeneratedBroadcastManifestChannel = {', + ' readonly name: string', + ' readonly pattern: string', + ' readonly type: \'public\' | \'private\' | \'presence\'', + ' readonly params: readonly string[]', + ' readonly whispers: readonly string[]', + ' readonly member?: unknown', + '}', + '', + 'type GeneratedBroadcastManifest = {', + ' readonly version: 1', + ' readonly generatedAt: string', + ' readonly events: readonly GeneratedBroadcastManifestEvent[]', + ' readonly channels: readonly GeneratedBroadcastManifestChannel[]', + '}', '', 'export const broadcastManifest = {', ' version: 1,', diff --git a/packages/cli/src/project/scaffold.ts b/packages/cli/src/project/scaffold.ts index 08112e5..fed290d 100644 --- a/packages/cli/src/project/scaffold.ts +++ b/packages/cli/src/project/scaffold.ts @@ -1727,6 +1727,8 @@ export async function syncManagedDriverDependencies( const cachePackageInstalled = typeof dependencies['@holo-js/cache'] !== 'undefined' || typeof devDependencies['@holo-js/cache'] !== 'undefined' + requiredPackages.add('@holo-js/core') + for (const connection of Object.values(loaded.database.connections)) { const inferredDriver = inferConnectionDriver(connection) if (inferredDriver) { @@ -1849,6 +1851,7 @@ export async function syncManagedDriverDependencies( let changed = false const nextVersion = `^${HOLO_PACKAGE_VERSION}` const removableManagedPackages = new Set([ + '@holo-js/core', ...Object.values(DB_DRIVER_PACKAGE_NAMES), '@holo-js/auth', '@holo-js/auth-clerk', @@ -3352,7 +3355,7 @@ function renderFrameworkRunner(options: Pick {', + ' if (buffered.length > 0) {', + ' onLine?.(buffered)', + ' }', ' if (buffered.length > 0 && !suppressedOutput.has(buffered)) {', ' target.write(buffered)', ' }', ' })', '}', '', + 'function extractNextConflictPid(lines) {', + ' if (framework !== \'next\' || mode !== \'dev\') {', + ' return undefined', + ' }', + '', + ' if (!lines.some(line => line.includes(\'Another next dev server is already running.\'))) {', + ' return undefined', + ' }', + '', + ' for (const line of lines) {', + ' const match = line.match(/^- PID:\\s+(\\d+)\\s*$/)', + ' if (match) {', + ' return Number.parseInt(match[1], 10)', + ' }', + ' }', + '', + ' return undefined', + '}', + '', + 'async function waitForProcessExit(pid, timeoutMs = 5000) {', + ' const deadline = Date.now() + timeoutMs', + ' while (Date.now() < deadline) {', + ' try {', + ' process.kill(pid, 0)', + ' } catch (error) {', + ' if (error && typeof error === \'object\' && \'code\' in error && error.code === \'ESRCH\') {', + ' return true', + ' }', + ' throw error', + ' }', + '', + ' await new Promise(resolve => setTimeout(resolve, 100))', + ' }', + '', + ' return false', + '}', + '', + 'async function stopStaleNextDevServer(pid) {', + ' if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) {', + ' return false', + ' }', + '', + ' try {', + ' process.kill(pid, \'SIGTERM\')', + ' } catch (error) {', + ' if (error && typeof error === \'object\' && \'code\' in error && error.code === \'ESRCH\') {', + ' return true', + ' }', + ' return false', + ' }', + '', + ' return waitForProcessExit(pid)', + '}', + '', 'if (!existsSync(binaryPath)) {', ' console.error(`[holo] Missing framework binary "${commandName}" for "${framework}". Run your package manager install first.`)', ' process.exit(1)', '}', '', - 'const child = spawn(binaryPath, commandArgs, {', - ' cwd: projectRoot,', - ' env: process.env,', - ' stdio: [\'inherit\', \'pipe\', \'pipe\'],', - '})', + 'let child = null', 'let forwardedSignal = null', '', - 'pipeOutput(child.stdout, process.stdout)', - 'pipeOutput(child.stderr, process.stderr)', - '', 'function forwardSignal(signal) {', - ' if (forwardedSignal || child.exitCode !== null) {', + ' if (forwardedSignal || !child || child.exitCode !== null) {', ' return', ' }', '', @@ -3408,15 +3462,51 @@ function renderFrameworkRunner(options: Pick {', + 'async function run() {', + ' let restartedAfterConflict = false', + '', + ' while (true) {', + ' const stderrLines = []', + ' child = spawn(binaryPath, commandArgs, {', + ' cwd: projectRoot,', + ' env: process.env,', + ' stdio: [\'inherit\', \'pipe\', \'pipe\'],', + ' })', + ' forwardedSignal = null', + '', + ' pipeOutput(child.stdout, process.stdout)', + ' pipeOutput(child.stderr, process.stderr, line => {', + ' stderrLines.push(line)', + ' })', + '', + ' const code = await new Promise((resolve, reject) => {', + ' child.on(\'error\', reject)', + ' child.on(\'close\', resolve)', + ' })', + '', + ' if (code === 0) {', + ' process.exit(0)', + ' }', + '', + ' const conflictPid = extractNextConflictPid(stderrLines)', + ' if (!restartedAfterConflict && typeof conflictPid === \'number\') {', + ' const stopped = await stopStaleNextDevServer(conflictPid)', + ' if (stopped) {', + ' restartedAfterConflict = true', + ' console.error(`[holo] Stopped stale Next dev server ${conflictPid}. Restarting dev server.`)', + ' continue', + ' }', + ' }', + '', + ' process.exit(code ?? 1)', + ' }', + '}', + '', + 'run().catch((error) => {', ' console.error(error instanceof Error ? error.message : String(error))', ' process.exit(1)', '})', '', - 'child.on(\'close\', (code) => {', - ' process.exit(code ?? 1)', - '})', - '', ].join('\n') } diff --git a/packages/cli/tests/broadcast-registry.test.ts b/packages/cli/tests/broadcast-registry.test.ts index f514ed1..52514b7 100644 --- a/packages/cli/tests/broadcast-registry.test.ts +++ b/packages/cli/tests/broadcast-registry.test.ts @@ -131,7 +131,8 @@ export default defineChannel('chat.{roomId}', { await expect(readFile(join(root, '.holo-js/generated/channels.ts'), 'utf8')).resolves.toContain('"whispers": [') await expect(readFile(join(root, '.holo-js/generated/broadcast-manifest.ts'), 'utf8')).resolves.toContain('events: [') await expect(readFile(join(root, '.holo-js/generated/broadcast-manifest.ts'), 'utf8')).resolves.toContain('"typing.start"') - await expect(readFile(join(root, '.holo-js/generated/broadcast-manifest.ts'), 'utf8')).resolves.toContain('import type { GeneratedBroadcastManifest } from \'@holo-js/core\'') + await expect(readFile(join(root, '.holo-js/generated/broadcast-manifest.ts'), 'utf8')).resolves.not.toContain('import type { GeneratedBroadcastManifest } from \'@holo-js/core\'') + await expect(readFile(join(root, '.holo-js/generated/broadcast-manifest.ts'), 'utf8')).resolves.toContain('type GeneratedBroadcastManifest = {') await expect(readFile(join(root, '.holo-js/generated/broadcast-manifest.ts'), 'utf8')).resolves.toContain('import type { ChannelPresenceMemberFor } from \'@holo-js/broadcast\'') await expect(readFile(join(root, '.holo-js/generated/broadcast-manifest.ts'), 'utf8')).resolves.toContain('member: undefined as unknown as ChannelPresenceMemberFor<"chat.{roomId}">') await expect(readFile(join(root, '.holo-js/generated/broadcast.d.ts'), 'utf8')).resolves.toContain('declare module \'@holo-js/broadcast\'') diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index fbdb7ab..caa6df9 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSyn import { chmod, mkdtemp, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { basename, dirname, extname, join, resolve } from 'node:path' -import { spawnSync } from 'node:child_process' +import { spawn, spawnSync } from 'node:child_process' import { PassThrough } from 'node:stream' import { EventEmitter } from 'node:events' import { pathToFileURL } from 'node:url' @@ -1417,6 +1417,35 @@ export default { expect(await readFile(join(nextRoot, 'server/holo.ts'), 'utf8')).toContain('./db/schema.generated') expect(await readFile(join(nextRoot, 'app/layout.tsx'), 'utf8')).toContain('../server/db/schema.generated') + const staleNextProcess = spawn('node', ['-e', 'setInterval(() => {}, 1000)'], { + stdio: 'ignore', + }) + const nextStatePath = join(nextRoot, '.next-runner-state') + await writeFile(join(nextRoot, 'node_modules/.bin/next'), `#!/usr/bin/env node +const { existsSync, readFileSync, writeFileSync } = require('node:fs') +const statePath = ${JSON.stringify(nextStatePath)} +const count = existsSync(statePath) ? Number.parseInt(readFileSync(statePath, 'utf8'), 10) : 0 +writeFileSync(statePath, String(count + 1)) +if (count === 0) { + console.error('⨯ Another next dev server is already running.') + console.error('') + console.error('- Local: http://localhost:53864') + console.error('- PID: ${staleNextProcess.pid}') + console.error('- Dir: ${nextRoot}') + console.error('- Log: .next/dev/logs/next-development.log') + process.exit(1) +} +console.log(process.argv.slice(2).join(' ')) +`, 'utf8') + await chmod(join(nextRoot, 'node_modules/.bin/next'), 0o755) + + const restartedDevResult = runNodeScript(nextRoot, join(nextRoot, '.holo-js/framework/run.mjs'), ['dev']) + expect(restartedDevResult.status, restartedDevResult.stderr || restartedDevResult.stdout).toBe(0) + expect(restartedDevResult.stdout).toContain('dev') + expect(restartedDevResult.stderr).toContain(`Stopped stale Next dev server ${staleNextProcess.pid}. Restarting dev server.`) + expect(await readFile(nextStatePath, 'utf8')).toBe('2') + await expect(async () => process.kill(staleNextProcess.pid!, 0)).rejects.toMatchObject({ code: 'ESRCH' }) + const svelteRoot = join(baseRoot, 'svelte-runner') await projectInternals.scaffoldProject(svelteRoot, { projectName: 'svelte-runner', @@ -2802,6 +2831,7 @@ export default defineStorageConfig({ await expect(projectInternals.syncManagedDriverDependencies(dependencySyncRoot)).resolves.toBe(true) expect(JSON.parse(await readFile(join(dependencySyncRoot, 'package.json'), 'utf8'))).toMatchObject({ dependencies: { + '@holo-js/core': expectedHoloPackageRange, '@holo-js/db': expectedHoloPackageRange, '@holo-js/db-postgres': expectedHoloPackageRange, '@holo-js/queue': expectedHoloPackageRange, @@ -9252,25 +9282,31 @@ throw 'string discovery failure' await new Promise(resolve => setTimeout(resolve, 5)) } + while (prepare.mock.calls.length < 1) { + await new Promise(resolve => setTimeout(resolve, 5)) + } + prepare.mockClear() + watchCallback('change', '.holo-js/generated/registry.json') await new Promise(resolve => setTimeout(resolve, 25)) - expect(prepare).toHaveBeenCalledTimes(1) + expect(prepare).toHaveBeenCalledTimes(0) watchCallback('change', 'server/db/schema.generated.ts') - while (prepare.mock.calls.length < 2) { + while (prepare.mock.calls.length < 1) { await new Promise(resolve => setTimeout(resolve, 5)) } - expect(prepare).toHaveBeenCalledTimes(2) + expect(prepare).toHaveBeenCalledTimes(1) + prepare.mockClear() watchCallback('change', 'config/app.ts') - while (prepare.mock.calls.length < 3) { + while (prepare.mock.calls.length < 1) { await new Promise(resolve => setTimeout(resolve, 5)) } child.emit('close', 0) await expect(devPromise).resolves.toBeUndefined() - expect(prepare).toHaveBeenCalledTimes(3) - }) + expect(prepare).toHaveBeenCalledTimes(1) + }, 30000) it('skips model files whose generated schema has not been materialized yet', async () => { const projectRoot = await createTempProject() @@ -9494,7 +9530,92 @@ export default defineConfig({ const svelteConfig = await readFile(join(projectRoot, 'svelte.config.js'), 'utf8') expect(svelteConfig).toContain('.holo-js/generated/hooks.server') expect(svelteConfig).toContain('.holo-js/generated/hooks') - }) + }, 30000) + + it('preserves legacy SvelteKit hook extension files when no migration occurs', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + await writeProjectFile(projectRoot, '.holo-js/framework/project.json', JSON.stringify({ framework: 'sveltekit' }, null, 2)) + await writeProjectFile(projectRoot, 'src/hooks.ts', 'export const reroute = ({ url }) => url.pathname\n') + await writeProjectFile(projectRoot, 'src/hooks.server.ts', 'export const handleFetch = async ({ request, fetch }) => fetch(request)\n') + await writeProjectFile(projectRoot, 'src/hooks.user.ts', 'export const transport = {}\n') + await writeProjectFile(projectRoot, 'src/hooks.server.user.ts', 'export const handle = async ({ event, resolve }) => resolve(event)\n') + await writeProjectFile(projectRoot, 'svelte.config.js', [ + 'import adapter from \'@sveltejs/adapter-node\'', + '', + 'const config = {', + ' kit: {', + ' adapter: adapter(),', + ' },', + '}', + '', + 'export default config', + '', + ].join('\n')) + + await withFakeBun(async () => { + await cliInternals.runProjectPrepare(projectRoot) + }) + + await expect(readFile(join(projectRoot, 'src/hooks.user.ts'), 'utf8')).resolves.toContain('export const transport') + await expect(readFile(join(projectRoot, 'src/hooks.server.user.ts'), 'utf8')).resolves.toContain('export const handle') + }, 30000) + + it('patches compact SvelteKit kit objects without producing invalid config', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + await writeProjectFile(projectRoot, '.holo-js/framework/project.json', JSON.stringify({ framework: 'sveltekit' }, null, 2)) + await writeProjectFile(projectRoot, 'svelte.config.js', [ + 'import adapter from \'@sveltejs/adapter-node\'', + '', + 'const config = {', + ' kit: { adapter: adapter() },', + '}', + '', + 'export default config', + '', + ].join('\n')) + + await withFakeBun(async () => { + await cliInternals.runProjectPrepare(projectRoot) + }) + + const svelteConfig = await readFile(join(projectRoot, 'svelte.config.js'), 'utf8') + expect(svelteConfig).toContain('kit: {') + expect(svelteConfig).toContain('adapter: adapter()') + expect(svelteConfig).toContain('.holo-js/generated/hooks.server') + expect(svelteConfig).toContain('.holo-js/generated/hooks') + expect(svelteConfig).toContain('files: {') + expect(svelteConfig).not.toContain('kit: { adapter: adapter() },\n files:') + }, 30000) + + it('fails prepare for unsupported custom SvelteKit hook entrypoints', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + await writeProjectFile(projectRoot, '.holo-js/framework/project.json', JSON.stringify({ framework: 'sveltekit' }, null, 2)) + await writeProjectFile(projectRoot, 'svelte.config.js', [ + 'import adapter from \'@sveltejs/adapter-node\'', + '', + 'const config = {', + ' kit: {', + ' adapter: adapter(),', + ' files: {', + ' hooks: {', + ' server: \'src/custom-hooks.server\',', + ' universal: \'src/custom-hooks\',', + ' },', + ' },', + ' },', + '}', + '', + 'export default config', + '', + ].join('\n')) + + await expect(withFakeBun(async () => { + await cliInternals.runProjectPrepare(projectRoot) + })).rejects.toThrow('Custom SvelteKit hook entrypoints are not supported') + }, 30000) it('fails holo dev when the child process errors or exits non-zero', async () => { const projectRoot = await createTempProject() diff --git a/packages/core/src/portable/holo.ts b/packages/core/src/portable/holo.ts index 1d326ee..15c6d4b 100644 --- a/packages/core/src/portable/holo.ts +++ b/packages/core/src/portable/holo.ts @@ -1,3 +1,4 @@ +import { existsSync } from 'node:fs' import { createHash, createHmac } from 'node:crypto' import { createRequire } from 'node:module' import { resolve } from 'node:path' @@ -19,9 +20,9 @@ import { DB, resetDB, } from '@holo-js/db' +import { importBundledRuntimeModule } from '../runtimeModule' import { resolveRuntimeConnectionManagerOptions } from './dbRuntime' import { loadGeneratedProjectRegistry, type GeneratedProjectRegistry } from './registry' -import { importBundledRuntimeModule } from '../runtimeModule' import { configurePlainNodeStorageRuntime, resetOptionalStorageRuntime } from '../storageRuntime' type RuntimeConfigRegistry = LoadedHoloConfig['all'] @@ -50,9 +51,13 @@ async function preloadGeneratedSchemaModule( return } - const expectedTarget = pathToFileURL(resolve(projectRoot, entry)).href + const expectedTarget = resolve(projectRoot, entry) + if (!existsSync(expectedTarget)) { + return + } + try { - await import(expectedTarget) + await importBundledRuntimeModule(projectRoot, entry) } catch (error) { if ( error instanceof Error @@ -61,7 +66,7 @@ async function preloadGeneratedSchemaModule( const message = error.message const failedTarget = message.match(/Cannot find module '([^']+)'|Cannot find package '([^']+)'|Failed to load url ([^ ]+)/)?.slice(1) .find((value): value is string => typeof value === 'string') - if (failedTarget === expectedTarget || failedTarget === resolve(projectRoot, entry)) { + if (failedTarget === expectedTarget) { return } } diff --git a/packages/db/src/model/ModelQueryBuilder.ts b/packages/db/src/model/ModelQueryBuilder.ts index dcd6cd0..ed1e5f2 100644 --- a/packages/db/src/model/ModelQueryBuilder.ts +++ b/packages/db/src/model/ModelQueryBuilder.ts @@ -809,15 +809,15 @@ export class ModelQueryBuilder< return this.applyMorphRelationFilter(relation, types, column, operator, value, 'or') } - whereBelongsTo( - relatedEntity: Entity, + whereBelongsTo( + relatedEntity: Entity, relationName?: ModelRelationPath, ): ModelQueryBuilder { return this.applyBelongsToFilter(relatedEntity, relationName, 'and') } - orWhereBelongsTo( - relatedEntity: Entity, + orWhereBelongsTo( + relatedEntity: Entity, relationName?: ModelRelationPath, ): ModelQueryBuilder { return this.applyBelongsToFilter(relatedEntity, relationName, 'or') @@ -1669,8 +1669,8 @@ export class ModelQueryBuilder< return alias ? { relation, alias } : { relation } } - private resolveBelongsToRelation( - relatedEntity: Entity, + private resolveBelongsToRelation( + relatedEntity: Entity, relationName?: ModelRelationPath, ): Extract['getRelationDefinition']>, { kind: 'belongsTo' }> { const resolvedName = this.resolveBelongsToRelationName(relatedEntity, relationName) @@ -1683,8 +1683,8 @@ export class ModelQueryBuilder< return relation } - private applyBelongsToFilter( - relatedEntity: Entity, + private applyBelongsToFilter( + relatedEntity: Entity, relationName: ModelRelationPath | undefined, boolean: 'and' | 'or', ): ModelQueryBuilder { @@ -1703,8 +1703,8 @@ export class ModelQueryBuilder< : this.orWhereHas(resolvedRelationName, query => query.where(relation.ownerKey, ownerValue)) } - private resolveBelongsToRelationName( - relatedEntity: Entity, + private resolveBelongsToRelationName( + relatedEntity: Entity, relationName?: ModelRelationPath, ): ModelRelationPath { if (relationName) { diff --git a/packages/db/src/model/defineModel.ts b/packages/db/src/model/defineModel.ts index e9c0ed2..ea74d30 100644 --- a/packages/db/src/model/defineModel.ts +++ b/packages/db/src/model/defineModel.ts @@ -297,8 +297,8 @@ type StaticModelApi< orWhereDoesntHave(relation: ModelRelationPath, constraint?: RelationConstraintCallback): ModelQueryBuilder whereRelation>(relation: TRelationPath, column: RelatedColumnNameForRelationPath, operator: unknown, value?: unknown): ModelQueryBuilder orWhereRelation>(relation: TRelationPath, column: RelatedColumnNameForRelationPath, operator: unknown, value?: unknown): ModelQueryBuilder - whereBelongsTo(entity: Entity, relationName?: ModelRelationPath): ModelQueryBuilder - orWhereBelongsTo(entity: Entity, relationName?: ModelRelationPath): ModelQueryBuilder + whereBelongsTo(entity: Entity, relationName?: ModelRelationPath): ModelQueryBuilder + orWhereBelongsTo(entity: Entity, relationName?: ModelRelationPath): ModelQueryBuilder whereMorphedTo(relation: ModelRelationPath, target: MorphTypeSelector): ModelQueryBuilder orWhereMorphedTo(relation: ModelRelationPath, target: MorphTypeSelector): ModelQueryBuilder whereNotMorphedTo(relation: ModelRelationPath, target: MorphTypeSelector): ModelQueryBuilder @@ -972,10 +972,10 @@ function createStaticModelApi< orWhereRelation>(relation: TRelationPath, column: RelatedColumnNameForRelationPath, operator: unknown, value?: unknown) { return this.query().orWhereRelation(relation, column, operator, value) }, - whereBelongsTo(entity: Entity, relationName?: ModelRelationPath) { + whereBelongsTo(entity: Entity, relationName?: ModelRelationPath) { return this.query().whereBelongsTo(entity, relationName) }, - orWhereBelongsTo(entity: Entity, relationName?: ModelRelationPath) { + orWhereBelongsTo(entity: Entity, relationName?: ModelRelationPath) { return this.query().orWhereBelongsTo(entity, relationName) }, whereMorphedTo(relation: ModelRelationPath, target: MorphTypeSelector) { diff --git a/packages/db/tests/model-relations.test.ts b/packages/db/tests/model-relations.test.ts index 279947d..3b25360 100644 --- a/packages/db/tests/model-relations.test.ts +++ b/packages/db/tests/model-relations.test.ts @@ -911,7 +911,6 @@ describe('model relation slice', () => { const user = await User.findOrFail(1) - // Use the dynamic method call syntax: user.roles().attach(...) const userDynamic = user as typeof user & { roles: () => { attach: (ids: unknown, attrs?: Record) => Promise, sync: (ids: unknown) => Promise<{ attached: unknown[], detached: unknown[], updated: unknown[] }>, detach: (ids?: unknown) => Promise } } await userDynamic.roles().attach([100, 101]) expect(adapter.tables.role_users).toHaveLength(2) @@ -926,6 +925,53 @@ describe('model relation slice', () => { expect(detached).toBe(1) }) + it('returns callable lazy relation properties that still resolve loaded values', async () => { + const adapter = new RelationAdapter({ + users: [{ id: 1, name: 'Mohamed' }], + roles: [ + { id: 100, name: 'Admin' }, + { id: 101, name: 'Editor' }, + ], + role_users: [ + { id: 1, userId: 1, roleId: 100 }, + { id: 2, userId: 1, roleId: 101 }, + ] }) + + configureDB(createConnectionManager({ + defaultConnection: 'default', + connections: { + default: createDatabase({ + connectionName: 'default', + adapter, + dialect: createDialect() }) } })) + + const users = defineTable('users', { + id: column.id(), + name: column.string() }) + const roles = defineTable('roles', { + id: column.id(), + name: column.string() }) + const roleUsers = defineTable('role_users', { + id: column.id(), + userId: column.integer(), + roleId: column.integer() }) + + const Role = defineModelFromTable(roles) + const User = defineModelFromTable(users, { + relations: { + roles: belongsToMany(() => Role, roleUsers, 'userId', 'roleId') } }) + + const user = await User.findOrFail(1) + const lazyUser = user as typeof user & { + roles: (() => { detach: (ids?: unknown) => Promise }) & Promise>[]> + } + + expect(typeof lazyUser.roles).toBe('function') + expect(await lazyUser.roles().detach([100])).toBe(1) + const loadedRoles = await lazyUser.roles + expect(loadedRoles).toHaveLength(1) + }) + it('supports dynamic relation method calls on defineModel(tableName) models with generated schema', async () => { const adapter = new RelationAdapter({ posts: [{ id: 1, title: 'Hello', user_id: 1, category_id: null, slug: 'hello', excerpt: null, body: 'content', status: 'published', published_at: null, created_at: '2026-01-01', updated_at: '2026-01-01' }], From 63ccee7cd6c8c3b6c13f3a29801395fa01c9382b Mon Sep 17 00:00:00 2001 From: Mohamed Melouk <42706279+cobraprojects@users.noreply.github.com> Date: Thu, 30 Apr 2026 00:07:37 +0300 Subject: [PATCH 2/3] fix next scaffolding --- packages/adapter-next/package.json | 5 ++ packages/adapter-next/src/config.ts | 66 +++++++++++++++++++++++ packages/adapter-next/src/index.ts | 2 + packages/adapter-next/tsup.config.ts | 1 + packages/cli/src/dev.ts | 7 +-- packages/cli/src/project/registry.ts | 4 +- packages/cli/src/project/scaffold.ts | 81 +++++++--------------------- packages/cli/tests/cli.test.ts | 4 ++ packages/core/src/portable/holo.ts | 4 +- packages/core/tsup.config.ts | 40 ++++++++++++++ 10 files changed, 146 insertions(+), 68 deletions(-) create mode 100644 packages/adapter-next/src/config.ts diff --git a/packages/adapter-next/package.json b/packages/adapter-next/package.json index d3755b0..5c29248 100644 --- a/packages/adapter-next/package.json +++ b/packages/adapter-next/package.json @@ -10,6 +10,11 @@ "import": "./dist/index.mjs", "default": "./dist/index.mjs" }, + "./config": { + "types": "./dist/config.d.ts", + "import": "./dist/config.mjs", + "default": "./dist/config.mjs" + }, "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.mjs", diff --git a/packages/adapter-next/src/config.ts b/packages/adapter-next/src/config.ts new file mode 100644 index 0000000..4b8c787 --- /dev/null +++ b/packages/adapter-next/src/config.ts @@ -0,0 +1,66 @@ +const HOLO_SERVER_EXTERNAL_PACKAGES = [ + '@holo-js/core', + '@holo-js/adapter-next', + '@holo-js/db', + '@holo-js/config', + 'esbuild', +] + +interface NextConfig { + readonly serverExternalPackages?: string[] + readonly outputFileTracingExcludes?: Record + readonly rewrites?: () => Promise + readonly [key: string]: unknown +} + +export function withHolo(nextConfig: TConfig = {} as TConfig): TConfig { + const existingExternal = nextConfig.serverExternalPackages ?? [] + const mergedExternal = [ + ...new Set([...HOLO_SERVER_EXTERNAL_PACKAGES, ...existingExternal]), + ] + + const existingExcludes = nextConfig.outputFileTracingExcludes ?? {} + const existingGlobalExcludes = existingExcludes['/*'] ?? [] + const mergedExcludes = { + ...existingExcludes, + '/*': [...new Set(['./next.config.ts', './next.config.mjs', ...existingGlobalExcludes])], + } + + const userRewrites = nextConfig.rewrites + + return { + ...nextConfig, + serverExternalPackages: mergedExternal, + outputFileTracingExcludes: mergedExcludes, + async rewrites() { + const userResult = await userRewrites?.() + + const raw = process.env.STORAGE_ROUTE_PREFIX?.trim() ?? '/storage' + const needsRewrite = raw && raw !== '/' && raw !== '/storage' + + if (!needsRewrite) { + return userResult ?? [] + } + + const storageRoutePrefix = `/${raw.replace(/^\/+|\/+$/g, '')}` + const holoRewrite = { + source: `${storageRoutePrefix}/:path*`, + destination: '/storage/:path*', + } + + if (Array.isArray(userResult)) { + return [...userResult, holoRewrite] + } + + if (userResult && typeof userResult === 'object' && !Array.isArray(userResult)) { + const shaped = userResult as { beforeFiles?: unknown[], afterFiles?: unknown[], fallback?: unknown[] } + return { + ...shaped, + beforeFiles: [...(shaped.beforeFiles ?? []), holoRewrite], + } + } + + return [holoRewrite] + }, + } +} diff --git a/packages/adapter-next/src/index.ts b/packages/adapter-next/src/index.ts index 025c74b..0ed67a6 100644 --- a/packages/adapter-next/src/index.ts +++ b/packages/adapter-next/src/index.ts @@ -37,4 +37,6 @@ export async function resetNextHoloProject(): Promise { await nextAdapter.resetProject() } +export { withHolo } from './config' + export const adapterNextInternals = nextAdapter.internals diff --git a/packages/adapter-next/tsup.config.ts b/packages/adapter-next/tsup.config.ts index fe1141e..fdeed32 100644 --- a/packages/adapter-next/tsup.config.ts +++ b/packages/adapter-next/tsup.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'tsup' export default defineConfig({ entry: { index: 'src/index.ts', + config: 'src/config.ts', client: 'src/client.ts', }, format: ['esm'], diff --git a/packages/cli/src/dev.ts b/packages/cli/src/dev.ts index 2f6fb3c..c86b3fd 100644 --- a/packages/cli/src/dev.ts +++ b/packages/cli/src/dev.ts @@ -158,6 +158,7 @@ async function refreshFrameworkRunner(projectRoot: string): Promise { const frameworkProjectPath = resolve(projectRoot, '.holo-js/framework/project.json') const frameworkRunnerPath = resolve(projectRoot, '.holo-js/framework/run.mjs') + let framework: 'next' | 'nuxt' | 'sveltekit' try { const content = await readFile(frameworkProjectPath, 'utf8') const manifest = JSON.parse(content) as { framework?: unknown } @@ -170,12 +171,12 @@ async function refreshFrameworkRunner(projectRoot: string): Promise { return } - await writeTextFile(frameworkRunnerPath, renderFrameworkRunner({ - framework: manifest.framework, - })) + framework = manifest.framework } catch { return } + + await writeTextFile(frameworkRunnerPath, renderFrameworkRunner({ framework })) } async function runNuxtPrepare(projectRoot: string): Promise { diff --git a/packages/cli/src/project/registry.ts b/packages/cli/src/project/registry.ts index ba1044a..f993d3d 100644 --- a/packages/cli/src/project/registry.ts +++ b/packages/cli/src/project/registry.ts @@ -540,10 +540,10 @@ export function renderGeneratedBroadcastManifest( 'type GeneratedBroadcastManifestChannel = {', ' readonly name: string', ' readonly pattern: string', - ' readonly type: \'public\' | \'private\' | \'presence\'', + ' readonly type: \'private\' | \'presence\'', ' readonly params: readonly string[]', ' readonly whispers: readonly string[]', - ' readonly member?: unknown', + ' readonly member?: Readonly>', '}', '', 'type GeneratedBroadcastManifest = {', diff --git a/packages/cli/src/project/scaffold.ts b/packages/cli/src/project/scaffold.ts index fed290d..5a3070e 100644 --- a/packages/cli/src/project/scaffold.ts +++ b/packages/cli/src/project/scaffold.ts @@ -2816,7 +2816,7 @@ function renderScaffoldTsconfig(options: Pick {', - ' const raw = process.env.STORAGE_ROUTE_PREFIX?.trim() ?? \'/storage\'', - ' if (!raw || raw === \'/\') {', - ' return \'/storage\'', - ' }', - '', - ' return `/${raw.replace(/^\\/+|\\/+$/g, \'\')}`', - '})()', + 'import type { NextConfig } from \'next\'', + 'import { withHolo } from \'@holo-js/adapter-next/config\'', '', - '/** @type {import(\'next\').NextConfig} */', - 'const nextConfig = {', - ' outputFileTracingExcludes: {', - ' \'/*\': [\'./next.config.mjs\'],', - ' },', - ' serverExternalPackages: [', - ' \'@holo-js/core\',', - ' \'@holo-js/adapter-next\',', - ' \'@holo-js/db\',', - ' \'@holo-js/config\',', - ' \'esbuild\',', - ' ],', - ' async rewrites() {', - ' if (storageRoutePrefix === \'/storage\') {', - ' return []', - ' }', - '', - ' return [', - ' {', - ' source: `${storageRoutePrefix}/:path*`,', - ' destination: \'/storage/:path*\',', - ' },', - ' ]', - ' },', - '}', + 'const nextConfig: NextConfig = withHolo({', + ' /* config options here */', + '})', '', 'export default nextConfig', '', @@ -3285,7 +3236,7 @@ function renderFrameworkFiles(options: ProjectScaffoldOptions): readonly Scaffol if (options.framework === 'next') { return [ - { path: 'next.config.mjs', contents: renderNextConfig(storageEnabled) }, + { path: 'next.config.ts', contents: renderNextConfig(storageEnabled) }, { path: 'next-env.d.ts', contents: renderNextEnvDts() }, { path: 'app/layout.tsx', contents: renderNextLayout(options.projectName) }, { path: 'app/page.tsx', contents: renderNextPage(options.projectName) }, @@ -3464,6 +3415,7 @@ function renderFrameworkRunner(options: Pick {', + ' if (stderrLines.length >= maxStderrLines) {', + ' stderrLines.shift()', + ' }', ' stderrLines.push(line)', ' })', '', - ' const code = await new Promise((resolve, reject) => {', + ' const result = await new Promise((resolve, reject) => {', ' child.on(\'error\', reject)', - ' child.on(\'close\', resolve)', + ' child.on(\'close\', (code, signal) => resolve({ code, signal }))', ' })', '', - ' if (code === 0) {', + ' if (result.code === 0) {', ' process.exit(0)', ' }', '', @@ -3498,7 +3453,11 @@ function renderFrameworkRunner(options: Pick {}, 1000)'], { stdio: 'ignore', }) + try { const nextStatePath = join(nextRoot, '.next-runner-state') await writeFile(join(nextRoot, 'node_modules/.bin/next'), `#!/usr/bin/env node const { existsSync, readFileSync, writeFileSync } = require('node:fs') @@ -1445,6 +1446,9 @@ console.log(process.argv.slice(2).join(' ')) expect(restartedDevResult.stderr).toContain(`Stopped stale Next dev server ${staleNextProcess.pid}. Restarting dev server.`) expect(await readFile(nextStatePath, 'utf8')).toBe('2') await expect(async () => process.kill(staleNextProcess.pid!, 0)).rejects.toMatchObject({ code: 'ESRCH' }) + } finally { + try { process.kill(staleNextProcess.pid!, 'SIGKILL') } catch { /* already exited */ } + } const svelteRoot = join(baseRoot, 'svelte-runner') await projectInternals.scaffoldProject(svelteRoot, { diff --git a/packages/core/src/portable/holo.ts b/packages/core/src/portable/holo.ts index 15c6d4b..4ad8ba5 100644 --- a/packages/core/src/portable/holo.ts +++ b/packages/core/src/portable/holo.ts @@ -61,10 +61,10 @@ async function preloadGeneratedSchemaModule( } catch (error) { if ( error instanceof Error - && /Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|Failed to load url/.test(error.message) + && /Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|Failed to load url|Failed to load /.test(error.message) ) { const message = error.message - const failedTarget = message.match(/Cannot find module '([^']+)'|Cannot find package '([^']+)'|Failed to load url ([^ ]+)/)?.slice(1) + const failedTarget = message.match(/Cannot find module '([^']+)'|Cannot find package '([^']+)'|Failed to load url ([^ ]+)|Failed to load ([^.]+)\./)?.slice(1) .find((value): value is string => typeof value === 'string') if (failedTarget === expectedTarget) { return diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index 513f1ee..ad1ad0a 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -1,7 +1,44 @@ import { defineConfig } from 'tsup' +import { readFile, writeFile, readdir } from 'node:fs/promises' +import { join } from 'node:path' const outDir = process.env.HOLO_BUILD_OUT_DIR ?? 'dist' +const builtinBareNames = [ + 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster', 'console', + 'constants', 'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', + 'fs/promises', 'http', 'http2', 'https', 'inspector', 'module', 'net', + 'os', 'path', 'perf_hooks', 'process', 'punycode', 'querystring', + 'readline', 'repl', 'stream', 'string_decoder', 'sys', 'timers', 'tls', + 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'wasi', + 'worker_threads', 'zlib', +] + +function buildBareToNodeRegex(): RegExp { + const escaped = builtinBareNames.map(n => n.replace('/', '\\/')) + return new RegExp( + `(from\\s+")(${escaped.join('|')})(")`, + 'g', + ) +} + +async function restoreNodeProtocol(dir: string): Promise { + const pattern = buildBareToNodeRegex() + const entries = await readdir(dir, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = join(dir, entry.name) + if (entry.isDirectory()) { + await restoreNodeProtocol(fullPath) + } else if (entry.name.endsWith('.mjs')) { + const content = await readFile(fullPath, 'utf8') + const updated = content.replace(pattern, '$1node:$2$3') + if (updated !== content) { + await writeFile(fullPath, updated, 'utf8') + } + } + } +} + export default defineConfig({ entry: { 'index': 'src/index.ts', @@ -15,4 +52,7 @@ export default defineConfig({ esbuildOptions(options) { options.logLevel = 'warning' }, + async onSuccess() { + await restoreNodeProtocol(outDir) + }, }) From ad83d6cf3a3c25146c31782ab6b1be3db8e8be2d Mon Sep 17 00:00:00 2001 From: Mohamed Melouk <42706279+cobraprojects@users.noreply.github.com> Date: Thu, 30 Apr 2026 00:41:50 +0300 Subject: [PATCH 3/3] fix findings --- packages/adapter-next/src/config.ts | 4 ++-- packages/cli/tests/cli.test.ts | 11 ++++++++++- packages/core/src/portable/holo.ts | 2 +- packages/core/tsup.config.ts | 8 ++++---- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/adapter-next/src/config.ts b/packages/adapter-next/src/config.ts index 4b8c787..3b8da55 100644 --- a/packages/adapter-next/src/config.ts +++ b/packages/adapter-next/src/config.ts @@ -33,7 +33,7 @@ export function withHolo(nextConfig: TConfig = {} as serverExternalPackages: mergedExternal, outputFileTracingExcludes: mergedExcludes, async rewrites() { - const userResult = await userRewrites?.() + const userResult = await userRewrites?.call(this) const raw = process.env.STORAGE_ROUTE_PREFIX?.trim() ?? '/storage' const needsRewrite = raw && raw !== '/' && raw !== '/storage' @@ -56,7 +56,7 @@ export function withHolo(nextConfig: TConfig = {} as const shaped = userResult as { beforeFiles?: unknown[], afterFiles?: unknown[], fallback?: unknown[] } return { ...shaped, - beforeFiles: [...(shaped.beforeFiles ?? []), holoRewrite], + beforeFiles: [...(Array.isArray(shaped.beforeFiles) ? shaped.beforeFiles : []), holoRewrite], } } diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index 8c3e5b0..f696f68 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -1447,7 +1447,16 @@ console.log(process.argv.slice(2).join(' ')) expect(await readFile(nextStatePath, 'utf8')).toBe('2') await expect(async () => process.kill(staleNextProcess.pid!, 0)).rejects.toMatchObject({ code: 'ESRCH' }) } finally { - try { process.kill(staleNextProcess.pid!, 'SIGKILL') } catch { /* already exited */ } + try { + process.kill(staleNextProcess.pid!, 'SIGKILL') + await new Promise((resolve) => { + const timeout = setTimeout(resolve, 2000) + staleNextProcess.once('exit', () => { + clearTimeout(timeout) + resolve() + }) + }) + } catch { /* already exited */ } } const svelteRoot = join(baseRoot, 'svelte-runner') diff --git a/packages/core/src/portable/holo.ts b/packages/core/src/portable/holo.ts index 4ad8ba5..6d0bcc5 100644 --- a/packages/core/src/portable/holo.ts +++ b/packages/core/src/portable/holo.ts @@ -64,7 +64,7 @@ async function preloadGeneratedSchemaModule( && /Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|Failed to load url|Failed to load /.test(error.message) ) { const message = error.message - const failedTarget = message.match(/Cannot find module '([^']+)'|Cannot find package '([^']+)'|Failed to load url ([^ ]+)|Failed to load ([^.]+)\./)?.slice(1) + const failedTarget = message.match(/Cannot find module '([^']+)'|Cannot find package '([^']+)'|Failed to load url ([^ ]+)|Failed to load ([^ ]+)\./)?.slice(1) .find((value): value is string => typeof value === 'string') if (failedTarget === expectedTarget) { return diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index ad1ad0a..9266e04 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -22,16 +22,16 @@ function buildBareToNodeRegex(): RegExp { ) } -async function restoreNodeProtocol(dir: string): Promise { - const pattern = buildBareToNodeRegex() +async function restoreNodeProtocol(dir: string, pattern?: RegExp): Promise { + const regex = pattern ?? buildBareToNodeRegex() const entries = await readdir(dir, { withFileTypes: true }) for (const entry of entries) { const fullPath = join(dir, entry.name) if (entry.isDirectory()) { - await restoreNodeProtocol(fullPath) + await restoreNodeProtocol(fullPath, regex) } else if (entry.name.endsWith('.mjs')) { const content = await readFile(fullPath, 'utf8') - const updated = content.replace(pattern, '$1node:$2$3') + const updated = content.replace(regex, '$1node:$2$3') if (updated !== content) { await writeFile(fullPath, updated, 'utf8') }