diff --git a/README.md b/README.md index 0614bea..5ac16b4 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,21 @@ bun run test bun run test:coverage ``` +### Dependency version policy + +Generated user projects must use publishable dependency ranges for `@holo-js/*` packages, for example `^0.1.4`. +Do not scaffold `workspace:*` into user apps. + +Committed apps under `apps/` are repo examples and test fixtures. They must use `workspace:*` for local +`@holo-js/*` packages so app validation runs against the current workspace code. They must also use `catalog:` for +dependencies that exist in the root workspace catalog. + +CLI dependency sync must preserve the app's existing Holo dependency mode. If an app already uses `workspace:*` for +any `@holo-js/*` package, newly managed `@holo-js/*` packages must also use `workspace:*`; otherwise they must use the +current publishable Holo range. + +This policy is enforced by `scripts/validate-dependency-version-policy.mjs`, which runs as part of `bun run test`. + For docs work: ```bash diff --git a/apps/blog-next/package.json b/apps/blog-next/package.json index 9ec3488..69593a3 100644 --- a/apps/blog-next/package.json +++ b/apps/blog-next/package.json @@ -17,42 +17,42 @@ }, "dependencies": { "@holo-js/adapter-next": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-github": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", - "@holo-js/cache-db": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-github": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", + "@holo-js/cache-db": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/flux-react": "workspace:*", "@holo-js/forms": "workspace:*", - "@holo-js/mail": "^0.1.4", - "@holo-js/notifications": "^0.1.4", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", + "@holo-js/mail": "workspace:*", + "@holo-js/notifications": "workspace:*", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", "@holo-js/validation": "workspace:*", - "esbuild": "^0.25.0", + "esbuild": "catalog:", "next": "catalog:", - "react": "^19.0.0", - "react-dom": "^19.0.0" + "react": "catalog:", + "react-dom": "catalog:" }, "devDependencies": { - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "typescript": "^5.8.0" + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "typescript": "catalog:" } } diff --git a/apps/blog-nuxt/package.json b/apps/blog-nuxt/package.json index 7476067..8075392 100644 --- a/apps/blog-nuxt/package.json +++ b/apps/blog-nuxt/package.json @@ -18,41 +18,41 @@ }, "dependencies": { "@holo-js/adapter-nuxt": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-github": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", - "@holo-js/cache-db": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-github": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", + "@holo-js/cache-db": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/flux-vue": "workspace:*", "@holo-js/forms": "workspace:*", - "@holo-js/mail": "^0.1.4", - "@holo-js/notifications": "^0.1.4", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", + "@holo-js/mail": "workspace:*", + "@holo-js/notifications": "workspace:*", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", "@holo-js/validation": "workspace:*", - "esbuild": "^0.25.0", + "esbuild": "catalog:", "nuxt": "catalog:", - "vue": "^3.5.13", - "vue-router": "^5.0.4" + "vue": "catalog:", + "vue-router": "catalog:" }, "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.8.0", - "vue-tsc": "^2.2.0" + "@types/node": "catalog:", + "typescript": "catalog:", + "vue-tsc": "catalog:" } } diff --git a/apps/blog-sveltekit/package.json b/apps/blog-sveltekit/package.json index 09e28aa..4ed4b38 100644 --- a/apps/blog-sveltekit/package.json +++ b/apps/blog-sveltekit/package.json @@ -17,42 +17,42 @@ }, "dependencies": { "@holo-js/adapter-sveltekit": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-github": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", - "@holo-js/cache-db": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-github": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", + "@holo-js/cache-db": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/flux-svelte": "workspace:*", "@holo-js/forms": "workspace:*", - "@holo-js/mail": "^0.1.4", - "@holo-js/notifications": "^0.1.4", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", + "@holo-js/mail": "workspace:*", + "@holo-js/notifications": "workspace:*", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", "@holo-js/validation": "workspace:*", "@sveltejs/adapter-node": "catalog:", "@sveltejs/kit": "catalog:", "@sveltejs/vite-plugin-svelte": "catalog:", - "esbuild": "^0.25.0", + "esbuild": "catalog:", "svelte": "catalog:", "vite": "catalog:" }, "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.8.0" + "@types/node": "catalog:", + "typescript": "catalog:" } } diff --git a/bun.lock b/bun.lock index d923d64..1c6d191 100644 --- a/bun.lock +++ b/bun.lock @@ -28,31 +28,31 @@ "name": "next_test_app", "dependencies": { "@holo-js/adapter-next": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-mysql": "^0.1.4", - "@holo-js/db-postgres": "^0.1.4", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-mysql": "workspace:*", + "@holo-js/db-postgres": "workspace:*", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/media": "workspace:*", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/queue-redis": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", - "@holo-js/storage-s3": "^0.1.4", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/queue-redis": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", + "@holo-js/storage-s3": "workspace:*", "esbuild": "^0.27.4", "next": "catalog:", "react": "^19.0.0", @@ -69,31 +69,31 @@ "name": "nuxt_test_app", "dependencies": { "@holo-js/adapter-nuxt": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-mysql": "^0.1.4", - "@holo-js/db-postgres": "^0.1.4", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-mysql": "workspace:*", + "@holo-js/db-postgres": "workspace:*", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/media": "workspace:*", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/queue-redis": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", - "@holo-js/storage-s3": "^0.1.4", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/queue-redis": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", + "@holo-js/storage-s3": "workspace:*", "esbuild": "^0.27.4", "nuxt": "^4.0.0", }, @@ -106,128 +106,128 @@ "name": "blog-next", "dependencies": { "@holo-js/adapter-next": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-github": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", - "@holo-js/cache-db": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-github": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", + "@holo-js/cache-db": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/flux-react": "workspace:*", "@holo-js/forms": "workspace:*", - "@holo-js/mail": "^0.1.4", - "@holo-js/notifications": "^0.1.4", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", + "@holo-js/mail": "workspace:*", + "@holo-js/notifications": "workspace:*", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", "@holo-js/validation": "workspace:*", - "esbuild": "^0.25.0", + "esbuild": "catalog:", "next": "catalog:", - "react": "^19.0.0", - "react-dom": "^19.0.0", + "react": "catalog:", + "react-dom": "catalog:", }, "devDependencies": { - "@types/node": "^22.0.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "typescript": "^5.8.0", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "typescript": "catalog:", }, }, "apps/blog-nuxt": { "name": "blog-nuxt", "dependencies": { "@holo-js/adapter-nuxt": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-github": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", - "@holo-js/cache-db": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-github": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", + "@holo-js/cache-db": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/flux-vue": "workspace:*", "@holo-js/forms": "workspace:*", - "@holo-js/mail": "^0.1.4", - "@holo-js/notifications": "^0.1.4", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", + "@holo-js/mail": "workspace:*", + "@holo-js/notifications": "workspace:*", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", "@holo-js/validation": "workspace:*", - "esbuild": "^0.25.0", + "esbuild": "catalog:", "nuxt": "catalog:", - "vue": "^3.5.13", - "vue-router": "^5.0.4", + "vue": "catalog:", + "vue-router": "catalog:", }, "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.8.0", - "vue-tsc": "^2.2.0", + "@types/node": "catalog:", + "typescript": "catalog:", + "vue-tsc": "catalog:", }, }, "apps/blog-sveltekit": { "name": "blog-sveltekit", "dependencies": { "@holo-js/adapter-sveltekit": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-github": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", - "@holo-js/cache-db": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-github": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", + "@holo-js/cache-db": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/flux-svelte": "workspace:*", "@holo-js/forms": "workspace:*", - "@holo-js/mail": "^0.1.4", - "@holo-js/notifications": "^0.1.4", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", + "@holo-js/mail": "workspace:*", + "@holo-js/notifications": "workspace:*", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", "@holo-js/validation": "workspace:*", "@sveltejs/adapter-node": "catalog:", "@sveltejs/kit": "catalog:", "@sveltejs/vite-plugin-svelte": "catalog:", - "esbuild": "^0.25.0", + "esbuild": "catalog:", "svelte": "catalog:", "vite": "catalog:", }, "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.8.0", + "@types/node": "catalog:", + "typescript": "catalog:", }, }, "apps/docs": { @@ -242,31 +242,31 @@ "name": "svelte_test_app", "dependencies": { "@holo-js/adapter-sveltekit": "workspace:*", - "@holo-js/auth": "^0.1.4", - "@holo-js/auth-clerk": "^0.1.4", - "@holo-js/auth-social": "^0.1.4", - "@holo-js/auth-social-google": "^0.1.4", - "@holo-js/auth-workos": "^0.1.4", - "@holo-js/authorization": "^0.1.4", - "@holo-js/broadcast": "^0.1.4", - "@holo-js/cache": "^0.1.4", + "@holo-js/auth": "workspace:*", + "@holo-js/auth-clerk": "workspace:*", + "@holo-js/auth-social": "workspace:*", + "@holo-js/auth-social-google": "workspace:*", + "@holo-js/auth-workos": "workspace:*", + "@holo-js/authorization": "workspace:*", + "@holo-js/broadcast": "workspace:*", + "@holo-js/cache": "workspace:*", "@holo-js/cli": "workspace:*", "@holo-js/config": "workspace:*", - "@holo-js/core": "^0.1.4", + "@holo-js/core": "workspace:*", "@holo-js/db": "workspace:*", - "@holo-js/db-mysql": "^0.1.4", - "@holo-js/db-postgres": "^0.1.4", - "@holo-js/db-sqlite": "^0.1.4", - "@holo-js/events": "^0.1.4", + "@holo-js/db-mysql": "workspace:*", + "@holo-js/db-postgres": "workspace:*", + "@holo-js/db-sqlite": "workspace:*", + "@holo-js/events": "workspace:*", "@holo-js/flux": "workspace:*", "@holo-js/media": "workspace:*", - "@holo-js/queue": "^0.1.4", - "@holo-js/queue-db": "^0.1.4", - "@holo-js/queue-redis": "^0.1.4", - "@holo-js/security": "^0.1.4", - "@holo-js/session": "^0.1.4", - "@holo-js/storage": "^0.1.4", - "@holo-js/storage-s3": "^0.1.4", + "@holo-js/queue": "workspace:*", + "@holo-js/queue-db": "workspace:*", + "@holo-js/queue-redis": "workspace:*", + "@holo-js/security": "workspace:*", + "@holo-js/session": "workspace:*", + "@holo-js/storage": "workspace:*", + "@holo-js/storage-s3": "workspace:*", "@sveltejs/adapter-node": "catalog:", "@sveltejs/kit": "catalog:", "@sveltejs/vite-plugin-svelte": "catalog:", @@ -288,12 +288,14 @@ }, "devDependencies": { "@types/node": "^22.10.2", + "next": "catalog:", "tsup": "^8.3.5", "typescript": "^5.7.2", "vitest": "catalog:", }, "peerDependencies": { "@holo-js/forms": "^0.1.4", + "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^18.3.1 || ^19.0.0", }, "optionalPeers": [ @@ -356,13 +358,13 @@ "@holo-js/config": "^0.1.4", }, "devDependencies": { - "@types/node": "^22.10.2", - "@types/react": "^19.0.0", + "@types/node": "catalog:", + "@types/react": "catalog:", "nuxt": "catalog:", - "react": "^19.0.0", + "react": "catalog:", "svelte": "catalog:", - "tsup": "^8.3.5", - "typescript": "^5.7.2", + "tsup": "catalog:", + "typescript": "catalog:", "vitest": "catalog:", }, "peerDependencies": { @@ -1088,6 +1090,8 @@ "@types/better-sqlite3": "^7.6.12", "@types/node": "^22.10.2", "@types/pg": "^8.11.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@vitest/coverage-istanbul": "^4.1.5", "@vitest/coverage-v8": "^4.1.5", "better-sqlite3": "^11.7.0", @@ -1101,6 +1105,8 @@ "next": "^16.2.4", "nuxt": "^4.4.4", "pg": "^8.13.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "svelte": "^5.55.5", "tslib": "^2.8.1", "tsup": "^8.3.5", @@ -2880,7 +2886,7 @@ "playground": ["playground@workspace:playground"], - "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], "postcss-calc": ["postcss-calc@10.1.1", "", { "dependencies": { "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.4.38" } }, "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw=="], @@ -3564,12 +3570,6 @@ "autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], - "blog-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=="], - - "blog-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=="], - - "blog-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=="], - "browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], @@ -3592,6 +3592,12 @@ "crc32-stream/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=="], + "cssnano/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "cssnano-preset-default/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "cssnano-utils/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -3630,14 +3636,14 @@ "mkdist/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + "mkdist/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "mkdist/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "mlly/ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], - "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "nitropack/@rollup/plugin-alias": ["@rollup/plugin-alias@6.0.0", "", { "peerDependencies": { "rollup": ">=4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g=="], "nitropack/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], @@ -3682,6 +3688,60 @@ "path-scurry/lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + "postcss-calc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-colormin/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-convert-values/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-discard-comments/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-discard-duplicates/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-discard-empty/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-discard-overridden/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-merge-longhand/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-merge-rules/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-minify-font-values/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-minify-gradients/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-minify-params/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-minify-selectors/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-charset/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-display-values/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-positions/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-repeat-style/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-string/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-timing-functions/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-unicode/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-url/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-normalize-whitespace/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-ordered-values/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-reduce-initial/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-reduce-transforms/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-svgo/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-unique-selectors/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], @@ -3710,6 +3770,8 @@ "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "stylehacks/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "sucrase/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "svgo/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -3840,6 +3902,8 @@ "@vitejs/plugin-vue/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "@vitejs/plugin-vue/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "@vue-macros/common/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], "@vue-macros/common/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], @@ -3848,6 +3912,8 @@ "@vue-macros/common/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vue-macros/common/@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.31", "", { "dependencies": { "@vue/compiler-dom": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog=="], @@ -3856,6 +3922,8 @@ "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vue/babel-plugin-resolve-type/@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "@vue/compiler-dom/@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.3", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA=="], @@ -3898,162 +3966,6 @@ "archiver/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "blog-next/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "blog-next/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "blog-next/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "blog-next/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "blog-next/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "blog-next/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "blog-next/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "blog-next/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "blog-next/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "blog-next/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "blog-next/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "blog-next/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "blog-next/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "blog-next/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "blog-next/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "blog-next/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "blog-next/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "blog-next/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "blog-next/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "blog-next/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "blog-next/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "blog-next/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "blog-next/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "blog-next/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "blog-next/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "blog-next/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - - "blog-nuxt/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "blog-nuxt/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "blog-nuxt/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "blog-nuxt/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "blog-nuxt/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "blog-nuxt/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "blog-nuxt/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "blog-nuxt/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "blog-nuxt/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "blog-nuxt/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "blog-nuxt/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "blog-nuxt/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "blog-nuxt/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "blog-nuxt/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "blog-nuxt/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "blog-nuxt/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "blog-nuxt/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "blog-nuxt/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "blog-nuxt/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "blog-nuxt/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "blog-nuxt/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "blog-nuxt/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "blog-nuxt/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "blog-nuxt/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "blog-nuxt/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "blog-nuxt/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - - "blog-sveltekit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "blog-sveltekit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "blog-sveltekit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "blog-sveltekit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "blog-sveltekit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "blog-sveltekit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "blog-sveltekit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "blog-sveltekit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "blog-sveltekit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "blog-sveltekit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "blog-sveltekit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "blog-sveltekit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "blog-sveltekit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "blog-sveltekit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "blog-sveltekit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "blog-sveltekit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "blog-sveltekit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "blog-sveltekit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "blog-sveltekit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "blog-sveltekit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "blog-sveltekit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "blog-sveltekit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "blog-sveltekit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "blog-sveltekit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "blog-sveltekit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "blog-sveltekit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], @@ -4322,22 +4234,34 @@ "vite-dev-rpc/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "vite-dev-rpc/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "vite-hot-client/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "vite-hot-client/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "vite-node/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "vite-node/vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "vite-plugin-checker/npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], "vite-plugin-checker/npm-run-path/unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "vite-plugin-inspect/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "vite-plugin-inspect/vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "vite-plugin-vue-tracer/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "vite-plugin-vue-tracer/vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "vitepress/@vue/devtools-api/@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="], "vitepress/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + "vitepress/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "vitepress/vue/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="], "vitepress/vue/@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.31", "", { "dependencies": { "@babel/parser": "^7.29.2", "@vue/compiler-core": "3.5.31", "@vue/compiler-dom": "3.5.31", "@vue/compiler-ssr": "3.5.31", "@vue/shared": "3.5.31", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.8", "source-map-js": "^1.2.1" } }, "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q=="], @@ -4480,6 +4404,8 @@ "@vueuse/core/vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vueuse/core/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "@vueuse/core/vue/@vue/runtime-dom/@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], "@vueuse/core/vue/@vue/runtime-dom/@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], @@ -4490,6 +4416,8 @@ "@vueuse/integrations/vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vueuse/integrations/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "@vueuse/integrations/vue/@vue/runtime-dom/@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], "@vueuse/integrations/vue/@vue/runtime-dom/@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], @@ -4500,6 +4428,8 @@ "@vueuse/shared/vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@vueuse/shared/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "@vueuse/shared/vue/@vue/runtime-dom/@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], "@vueuse/shared/vue/@vue/runtime-dom/@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], @@ -4668,6 +4598,8 @@ "vitepress/vue/@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "vitepress/vue/@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + "vitepress/vue/@vue/runtime-dom/@vue/reactivity": ["@vue/reactivity@3.5.31", "", { "dependencies": { "@vue/shared": "3.5.31" } }, "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g=="], "vitepress/vue/@vue/runtime-dom/@vue/runtime-core": ["@vue/runtime-core@3.5.31", "", { "dependencies": { "@vue/reactivity": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q=="], diff --git a/package.json b/package.json index 96a670c..5ab4783 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "vite": "^8.0.10", "svelte": "^5.55.5", "next": "^16.2.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", "@sveltejs/kit": "^2.59.1", "@sveltejs/vite-plugin-svelte": "^7.1.0", "@sveltejs/adapter-node": "^5.5.4", @@ -62,7 +66,7 @@ "typecheck:libraries": "bun run --workspaces --if-present --sequential typecheck", "typecheck:tests": "node scripts/run-test-typecheck.mjs", "typecheck:db": "bun run --filter @holo-js/db typecheck", - "test": "bun run --workspaces --if-present --sequential test", + "test": "bun run test:dependency-policy && bun run --workspaces --if-present --sequential test", "test:watch": "vitest", "test:db": "bun run --cwd packages/db test", "test:media": "bun run --cwd packages/media test", @@ -73,6 +77,7 @@ "test:example:blog-next": "bun run --cwd apps/blog-next test", "test:example:blog-sveltekit": "bun run --cwd apps/blog-sveltekit test", "test:examples": "bun run test:example:blog-nuxt && bun run test:example:blog-next && bun run test:example:blog-sveltekit", + "test:dependency-policy": "node --test scripts/validate-dependency-version-policy.test.mjs && node scripts/validate-dependency-version-policy.mjs", "test:config:coverage": "rm -rf coverage/config && bun run --cwd packages/config test -- --coverage --coverage.clean=false --coverage.reportsDirectory='../../coverage/config' --coverage.include='src/**/*.ts' --coverage.exclude='src/**/types.ts'", "test:core:coverage": "rm -rf coverage/core && bun run --cwd packages/core test -- --coverage --coverage.clean=false --coverage.reportsDirectory='../../coverage/core' --coverage.include='src/**/*.ts' --coverage.exclude='src/**/types.ts' --coverage.exclude='src/runtime/**/*.d.ts'", "test:db:coverage": "rm -rf coverage/db && bun run --cwd packages/db test -- --coverage --coverage.clean=false --coverage.reportsDirectory='../../coverage/db' --coverage.include='src/**/*.ts' --coverage.exclude='src/**/types.ts' --coverage.exclude='src/migrations/templates/**' --coverage.exclude='src/drivers/index.ts' --coverage.exclude='src/drivers/SQLiteAdapter.ts' --coverage.exclude='src/drivers/PostgresAdapter.ts' --coverage.exclude='src/drivers/MySQLAdapter.ts'", diff --git a/packages/adapter-next/package.json b/packages/adapter-next/package.json index d3a4a8c..2d1f977 100644 --- a/packages/adapter-next/package.json +++ b/packages/adapter-next/package.json @@ -43,6 +43,7 @@ }, "peerDependencies": { "@holo-js/forms": "^0.1.4", + "next": "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", "react": "^18.3.1 || ^19.0.0" }, "peerDependenciesMeta": { @@ -52,6 +53,7 @@ }, "devDependencies": { "@types/node": "^22.10.2", + "next": "catalog:", "tsup": "^8.3.5", "typescript": "^5.7.2", "vitest": "catalog:" diff --git a/packages/adapter-next/tests/adapter.type.test.ts b/packages/adapter-next/tests/adapter.type.test.ts index d7cdf09..b079110 100644 --- a/packages/adapter-next/tests/adapter.type.test.ts +++ b/packages/adapter-next/tests/adapter.type.test.ts @@ -110,7 +110,18 @@ describe('@holo-js/adapter-next typing', () => { [ `import { AuthProvider, useAuth, type HoloAuthUser, type UseAuthResult } from '@holo-js/auth/next/client'`, `import { auth } from '@holo-js/auth/next/server'`, + `import { field, schema } from '@holo-js/forms'`, `import { useForm } from '@holo-js/adapter-next/client'`, + `const loginForm = schema({ email: field.string().required().email() })`, + `useForm(loginForm, {`, + ` initialValues: { email: '' },`, + ` async submitter({ formData, values }) {`, + ` const email: string = values.email`, + ` const submittedEmail = formData.get('email')`, + ` void submittedEmail`, + ` return { ok: true, status: 200, data: { email } }`, + ` },`, + `})`, `const currentAuth = useAuth()`, `const user: HoloAuthUser | null = currentAuth.user`, `const authResult: UseAuthResult = currentAuth`, @@ -132,6 +143,7 @@ describe('@holo-js/adapter-next typing', () => { 'nodenext', '--target', 'es2022', + '--strict', '--noEmit', entryPath, ], diff --git a/packages/auth/package.json b/packages/auth/package.json index 84ff7fd..54db114 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -81,13 +81,13 @@ } }, "devDependencies": { - "@types/react": "^19.0.0", + "@types/react": "catalog:", "nuxt": "catalog:", - "react": "^19.0.0", + "react": "catalog:", "svelte": "catalog:", - "@types/node": "^22.10.2", - "tsup": "^8.3.5", - "typescript": "^5.7.2", + "@types/node": "catalog:", + "tsup": "catalog:", + "typescript": "catalog:", "vitest": "catalog:" } } diff --git a/packages/cache-redis/src/index.ts b/packages/cache-redis/src/index.ts index 07ed20d..1e64c4d 100644 --- a/packages/cache-redis/src/index.ts +++ b/packages/cache-redis/src/index.ts @@ -93,6 +93,7 @@ type RedisClusterOptions = { type RedisClientLike = { readonly isCluster?: boolean + disconnect?(): void get(key: string): Promise set(key: string, value: string, ...arguments_: readonly (string | number)[]): Promise<'OK' | null> del(...keys: string[]): Promise @@ -111,6 +112,7 @@ type RedisCtor = typeof Redis & { } const REDIS_SCAN_COUNT = 100 +const CACHE_DRIVER_DISPOSE_SYMBOL = Symbol.for('holo.cache.driver.dispose') const RELEASE_LOCK_SCRIPT = [ 'if redis.call("get", KEYS[1]) == ARGV[1] then', ' return redis.call("del", KEYS[1])', @@ -358,7 +360,7 @@ export function createRedisCacheDriver(options: RedisCacheDriverOptions): CacheD } while (cursor !== '0') } - return { + const driver: CacheDriverContract = { name: options.name, driver: 'redis', async get(key: string): Promise { @@ -443,6 +445,14 @@ export function createRedisCacheDriver(options: RedisCacheDriverOptions): CacheD return createRedisLock(client, name, seconds, ownerFactory, sleep, now) }, } + + Object.defineProperty(driver, CACHE_DRIVER_DISPOSE_SYMBOL, { + value() { + client.disconnect?.() + }, + }) + + return driver } export const redisCacheDriverInternals = { diff --git a/packages/cache-redis/tests/package.test.ts b/packages/cache-redis/tests/package.test.ts index 189edf9..81b9573 100644 --- a/packages/cache-redis/tests/package.test.ts +++ b/packages/cache-redis/tests/package.test.ts @@ -6,6 +6,7 @@ const redisMock = vi.hoisted(() => { const calls = { constructorArgs: [] as unknown[][], del: [] as string[][], + disconnect: [] as true[], eval: [] as Array<[string, number, ...string[]]>, get: [] as string[], incrby: [] as Array<[string, number]>, @@ -85,6 +86,10 @@ const redisMock = vi.hoisted(() => { return new FakeRedis().eval(script, numberOfKeys, ...arguments_) } + disconnect(): void { + calls.disconnect.push(true) + } + nodes(): readonly FakeRedis[] { return [new FakeRedis(), new FakeRedis()] } @@ -94,6 +99,10 @@ const redisMock = vi.hoisted(() => { calls.constructorArgs.push(args) } + disconnect(): void { + calls.disconnect.push(true) + } + async get(key: string): Promise { calls.get.push(key) if (isExpired(key, Date.now())) { @@ -203,6 +212,7 @@ const redisMock = vi.hoisted(() => { lockOwners.clear() calls.constructorArgs.length = 0 calls.del.length = 0 + calls.disconnect.length = 0 calls.eval.length = 0 calls.get.length = 0 calls.incrby.length = 0 @@ -220,6 +230,8 @@ vi.mock('ioredis', () => ({ import { CacheInvalidNumericMutationError } from '@holo-js/cache' import { createRedisCacheDriver, redisCacheDriverInternals } from '../src/index' +const cacheDriverDisposeSymbol = Symbol.for('holo.cache.driver.dispose') + describe('@holo-js/cache-redis', () => { beforeEach(() => { redisMock.reset() @@ -276,6 +288,28 @@ describe('@holo-js/cache-redis', () => { ]) }) + it('disconnects its redis client through the runtime lifecycle hook', () => { + const driver = createRedisCacheDriver({ + name: 'redis', + connectionName: 'cache', + prefix: 'holo:cache:', + redis: { + host: '127.0.0.1', + port: 6379, + db: 0, + }, + }) as ReturnType & Record void> + + const dispose = driver[cacheDriverDisposeSymbol] + if (!dispose) { + throw new Error('Expected redis cache driver to expose its runtime lifecycle hook.') + } + + dispose() + + expect(redisMock.calls.disconnect).toEqual([true]) + }) + it('supports expiration and immediate-expiry writes', async () => { vi.useFakeTimers() vi.setSystemTime(new Date('2026-04-22T00:00:00.000Z')) @@ -435,6 +469,33 @@ describe('@holo-js/cache-redis', () => { expect(redisMock.calls.del.every(keys => keys.length === 1)).toBe(true) }) + it('handles cluster clients that do not expose master node iteration', async () => { + const clusterPrototype = redisMock.FakeRedis.Cluster.prototype as { + nodes?: (role: 'master') => readonly unknown[] + } + const originalNodes = clusterPrototype.nodes + clusterPrototype.nodes = undefined + + try { + const driver = createRedisCacheDriver({ + name: 'redis-cluster', + connectionName: 'cache', + prefix: 'holo:cache:', + redis: { + db: 0, + clusters: [ + { host: 'cache-a.internal', port: 6379 }, + ], + }, + }) + + await expect(driver.flush()).resolves.toBeUndefined() + expect(redisMock.calls.scan).toEqual([]) + } finally { + clusterPrototype.nodes = originalNodes + } + }) + it('prefers url, then clusters, then host/socket when creating redis clients', async () => { createRedisCacheDriver({ name: 'by-url', @@ -559,6 +620,7 @@ describe('@holo-js/cache-redis', () => { }) await expect(driver.increment('holo:cache:wrongtype', 1)).rejects.toThrow(CacheInvalidNumericMutationError) + await expect(driver.increment('holo:cache:timeout', 1)).rejects.toThrow('ETIMEDOUT') await expect(driver.decrement('holo:cache:timeout', 1)).rejects.toThrow('ETIMEDOUT') expect(redisCacheDriverInternals.isRedisNumericMutationError(new Error('WRONGTYPE boom'))).toBe(true) expect(redisCacheDriverInternals.isRedisNumericMutationError(new Error('ETIMEDOUT'))).toBe(false) diff --git a/packages/cache-redis/tests/real-redis.test.ts b/packages/cache-redis/tests/real-redis.test.ts new file mode 100644 index 0000000..221655b --- /dev/null +++ b/packages/cache-redis/tests/real-redis.test.ts @@ -0,0 +1,57 @@ +import { randomUUID } from 'node:crypto' +import { afterEach, describe, expect, it } from 'vitest' +import cache, { configureCacheRuntime, resetCacheRuntime } from '@holo-js/cache' +import { createRedisCacheDriver } from '../src/index' + +describe('@holo-js/cache-redis real redis integration', () => { + afterEach(() => { + resetCacheRuntime() + }) + + it('works through the public cache facade and releases redis through runtime reset', async () => { + const prefix = `holo:test:cache:${randomUUID()}:` + + configureCacheRuntime({ + config: { + default: 'redis', + drivers: { + redis: { + driver: 'redis', + prefix, + }, + }, + }, + drivers: new Map([ + ['redis', createRedisCacheDriver({ + name: 'redis', + connectionName: 'cache', + prefix, + redis: { + host: '127.0.0.1', + port: 6379, + db: 0, + }, + })], + ]), + }) + + try { + await cache.put('children', { count: 2 }, 60) + expect(await cache.get('children')).toEqual({ count: 2 }) + + expect(await cache.add('children', { count: 3 }, 60)).toBe(false) + expect(await cache.increment('counter', 3)).toBe(3) + expect(await cache.decrement('counter')).toBe(2) + expect(await cache.lock('children:refresh', 1).get(async () => 'locked')).toBe('locked') + + await cache.flush() + expect(await cache.get('children')).toBeNull() + } finally { + try { + await cache.flush() + } finally { + resetCacheRuntime() + } + } + }) +}) diff --git a/packages/cache/src/redis.ts b/packages/cache/src/redis.ts index 6cf115f..79b25c0 100644 --- a/packages/cache/src/redis.ts +++ b/packages/cache/src/redis.ts @@ -24,6 +24,11 @@ type RedisCacheDriverModule = { } type RedisDriverModuleLoader = () => Promise +const CACHE_DRIVER_DISPOSE_SYMBOL = Symbol.for('holo.cache.driver.dispose') + +type DisposableCacheDriver = CacheDriverContract & { + readonly [CACHE_DRIVER_DISPOSE_SYMBOL]?: () => void +} function isNormalizedRedisConfig( config: HoloRedisConfig | NormalizedHoloRedisConfig, @@ -114,6 +119,7 @@ class LazyRedisCacheDriver implements CacheDriverContract { private driverInstance?: CacheDriverContract private pending?: Promise + private disposalGeneration = 0 constructor(private readonly options: RedisCacheDriverOptions) {} @@ -124,9 +130,12 @@ class LazyRedisCacheDriver implements CacheDriverContract { private async resolveDriver(): Promise { if (this.driverInstance) return this.driverInstance + const pendingGeneration = this.disposalGeneration this.pending ??= redisDriverModuleLoader().then((module) => { const driver = module.createRedisCacheDriver(this.options) - this.driverInstance = driver + if (this.disposalGeneration === pendingGeneration) { + this.driverInstance = driver + } return driver }).finally(() => { this.pending = undefined @@ -141,6 +150,26 @@ class LazyRedisCacheDriver implements CacheDriverContract { return callback(await this.resolveDriver()) } + [CACHE_DRIVER_DISPOSE_SYMBOL](): void { + const pending = this.pending + const driverInstance = this.driverInstance + + this.disposalGeneration += 1 + this.driverInstance = undefined + this.pending = undefined + + if (driverInstance) { + const disposable = driverInstance as DisposableCacheDriver + disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.() + return + } + + pending?.then((driver) => { + const disposable = driver as DisposableCacheDriver + disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.() + }).catch(() => {}) + } + private createLockProxy(name: string, seconds: number): CacheLockContract { let lockPromise: Promise | undefined diff --git a/packages/cache/src/runtime-shared.ts b/packages/cache/src/runtime-shared.ts index 65d6850..792b34c 100644 --- a/packages/cache/src/runtime-shared.ts +++ b/packages/cache/src/runtime-shared.ts @@ -30,6 +30,31 @@ type RuntimeCacheState = { bindings?: CacheRuntimeFacade } +const CACHE_DRIVER_DISPOSE_SYMBOL = Symbol.for('holo.cache.driver.dispose') + +type DisposableCacheDriver = CacheDriverContract & { + readonly [CACHE_DRIVER_DISPOSE_SYMBOL]?: () => void +} + +function disposeDriver(driver: CacheDriverContract): void { + const disposable = driver as DisposableCacheDriver + disposable[CACHE_DRIVER_DISPOSE_SYMBOL]?.() +} + +export function disposeCacheRuntimeBindings(bindings: CacheRuntimeFacade | undefined): void { + if (!bindings) { + return + } + + for (const [driverName, driver] of bindings.drivers.entries()) { + try { + disposeDriver(driver) + } catch (error) { + console.error(`[@holo-js/cache] Failed to dispose cache driver "${driverName}".`, error) + } + } +} + export function isNormalizedCacheConfig( config: HoloCacheConfig | NormalizedHoloCacheConfig, ): config is NormalizedHoloCacheConfig { diff --git a/packages/cache/src/runtime.ts b/packages/cache/src/runtime.ts index daf27ee..a26739a 100644 --- a/packages/cache/src/runtime.ts +++ b/packages/cache/src/runtime.ts @@ -9,6 +9,7 @@ import { import { cacheRedisInternals } from './redis' import { createDriverMap, + disposeCacheRuntimeBindings, getCacheRuntimeState, isNormalizedCacheConfig, normalizeRuntimeConfig, @@ -18,6 +19,8 @@ import { export { getCacheRuntime, getCacheRuntimeBindings } from './runtime-shared' export function configureCacheRuntime(bindings?: CacheRuntimeBindings): void { + disposeCacheRuntimeBindings(getCacheRuntimeState().bindings) + if (!bindings) { getCacheRuntimeState().bindings = undefined resetDefaultDependencyIndex() @@ -40,6 +43,7 @@ export function configureCacheRuntime(bindings?: CacheRuntimeBindings): void { } export function resetCacheRuntime(): void { + disposeCacheRuntimeBindings(getCacheRuntimeState().bindings) getCacheRuntimeState().bindings = undefined resetDefaultDependencyIndex() setGlobalDatabaseQueryCacheBridge(undefined) diff --git a/packages/cache/tests/package.test.ts b/packages/cache/tests/package.test.ts index 43a4f94..6524c7a 100644 --- a/packages/cache/tests/package.test.ts +++ b/packages/cache/tests/package.test.ts @@ -35,6 +35,7 @@ import cache, { } from '../src' const typedThemeKey = defineCacheKey<'light' | 'dark'>('theme.current') +const cacheDriverDisposeSymbol = Symbol.for('holo.cache.driver.dispose') async function createTempCacheDirectory(name: string): Promise { return mkdtemp(join(tmpdir(), `holo-cache-${name}-`)) @@ -1072,6 +1073,7 @@ describe('@holo-js/cache package surface', () => { it('lazy-loads redis drivers through shared redis config resolution', async () => { const values = new Map() + const disposeRedisDriver = vi.fn() const createRedisCacheDriver = vi.fn((options: { readonly name: string readonly connectionName: string @@ -1090,6 +1092,7 @@ describe('@holo-js/cache package surface', () => { return { name: options.name, driver: 'redis' as const, + [cacheDriverDisposeSymbol]: disposeRedisDriver, async get(key: string) { return values.has(key) ? Object.freeze({ @@ -1180,6 +1183,279 @@ describe('@holo-js/cache package surface', () => { await expect(cache.lock('report', 1).block(0)).resolves.toBe(true) expect(createRedisCacheDriver).toHaveBeenCalledTimes(1) expect(cacheRuntimeInternals.resolveConfiguredDriver(getCacheRuntime()).driver).toBe('redis') + + resetCacheRuntime() + expect(disposeRedisDriver).toHaveBeenCalledTimes(1) + }) + + it('disposes a lazy redis driver when reset happens while the optional module is loading', async () => { + let resolveModule: ((module: { + readonly createRedisCacheDriver: () => ReturnType + }) => void) | undefined + const disposeRedisDriver = vi.fn() + + function createPendingRedisDriver() { + return { + name: 'redis', + driver: 'redis' as const, + [cacheDriverDisposeSymbol]: disposeRedisDriver, + async get() { + return Object.freeze({ hit: false }) + }, + async put() { + return true + }, + async add() { + return true + }, + async forget() { + return true + }, + async flush() {}, + async increment() { + return 1 + }, + async decrement() { + return -1 + }, + lock(name: string) { + return { + name, + async get(callback?: () => TValue | Promise) { + return callback ? callback() : true + }, + async release() { + return true + }, + async block(_waitSeconds: number, callback?: () => TValue | Promise) { + return callback ? callback() : true + }, + } + }, + } + } + + cacheRedisInternals.setRedisDriverModuleLoader(() => new Promise((resolve) => { + resolveModule = resolve + })) + + configureCacheRuntime({ + config: { + default: 'redis', + drivers: { + redis: { + driver: 'redis', + connection: 'cache', + }, + }, + }, + redisConfig: { + default: 'cache', + connections: { + cache: { + host: '127.0.0.1', + port: 6379, + }, + }, + }, + }) + + const write = cache.put('alpha', 'one', 60) + resetCacheRuntime() + resolveModule?.({ + createRedisCacheDriver: createPendingRedisDriver, + }) + + await expect(write).resolves.toBe(true) + await vi.waitFor(() => { + expect(disposeRedisDriver).toHaveBeenCalledTimes(1) + }) + }) + + it('does not cache a lazy redis driver that resolves after runtime disposal', async () => { + let resolveModule: ((module: { + readonly createRedisCacheDriver: () => ReturnType + }) => void) | undefined + const disposeRedisDriver = vi.fn() + const getRedisValue = vi.fn(async () => Object.freeze({ hit: false })) + const createRedisCacheDriver = vi.fn(createPendingRedisDriver) + + function createPendingRedisDriver() { + return { + name: 'redis', + driver: 'redis' as const, + [cacheDriverDisposeSymbol]: disposeRedisDriver, + get: getRedisValue, + async put() { + return true + }, + async add() { + return true + }, + async forget() { + return true + }, + async flush() {}, + async increment() { + return 1 + }, + async decrement() { + return -1 + }, + lock(name: string) { + return { + name, + async get(callback?: () => TValue | Promise) { + return callback ? callback() : true + }, + async release() { + return true + }, + async block(_waitSeconds: number, callback?: () => TValue | Promise) { + return callback ? callback() : true + }, + } + }, + } + } + + cacheRedisInternals.setRedisDriverModuleLoader(() => new Promise((resolve) => { + resolveModule = resolve + })) + + configureCacheRuntime({ + config: { + default: 'redis', + drivers: { + redis: { + driver: 'redis', + connection: 'cache', + }, + }, + }, + redisConfig: { + default: 'cache', + connections: { + cache: { + host: '127.0.0.1', + port: 6379, + }, + }, + }, + }) + + const write = cache.put('alpha', 'one', 60) + resetCacheRuntime() + resolveModule?.({ createRedisCacheDriver }) + await expect(write).resolves.toBe(true) + + cacheRedisInternals.setRedisDriverModuleLoader(async () => ({ createRedisCacheDriver })) + configureCacheRuntime({ + config: { + default: 'redis', + drivers: { + redis: { + driver: 'redis', + connection: 'cache', + }, + }, + }, + redisConfig: { + default: 'cache', + connections: { + cache: { + host: '127.0.0.1', + port: 6379, + }, + }, + }, + }) + + await expect(cache.get('alpha')).resolves.toBeNull() + expect(createRedisCacheDriver).toHaveBeenCalledTimes(2) + expect(getRedisValue).toHaveBeenCalledTimes(1) + expect(disposeRedisDriver).toHaveBeenCalledTimes(1) + }) + + it('continues disposing runtime drivers when one driver dispose hook throws', () => { + const firstDispose = vi.fn(() => { + throw new Error('first dispose failed') + }) + const secondDispose = vi.fn() + const reportError = vi.spyOn(console, 'error').mockImplementation(() => {}) + const driver = { + name: 'first', + driver: 'memory' as const, + [cacheDriverDisposeSymbol]: firstDispose, + async get() { + return Object.freeze({ hit: false }) + }, + async put() { + return true + }, + async add() { + return true + }, + async forget() { + return true + }, + async flush() {}, + async increment() { + return 1 + }, + async decrement() { + return 1 + }, + lock(name: string) { + return { + name, + async get() { + return true + }, + async release() { + return true + }, + async block() { + return true + }, + } + }, + } + const secondDriver = { + ...driver, + name: 'second', + [cacheDriverDisposeSymbol]: secondDispose, + } + + try { + configureCacheRuntime({ + config: defineCacheConfig({ + default: 'first', + drivers: { + first: { + driver: 'memory', + }, + second: { + driver: 'memory', + }, + }, + }), + drivers: new Map([ + ['first', driver], + ['second', secondDriver], + ]), + }) + + expect(() => resetCacheRuntime()).not.toThrow() + expect(firstDispose).toHaveBeenCalledTimes(1) + expect(secondDispose).toHaveBeenCalledTimes(1) + expect(reportError).toHaveBeenCalledWith( + expect.stringContaining('first'), + expect.any(Error), + ) + } finally { + reportError.mockRestore() + } }) it('throws a clear error when redis cache support is configured without the optional package', async () => { diff --git a/packages/cli/package.json b/packages/cli/package.json index 000d051..46895ff 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,8 +20,8 @@ "dist" ], "scripts": { - "build": "tsup", - "stub": "tsup --watch", + "build": "node ../../scripts/generate-cli-workspace-catalog.mjs && tsup", + "stub": "node ../../scripts/generate-cli-workspace-catalog.mjs && tsup --watch", "typecheck": "tsc -p tsconfig.json --noEmit", "test": "vitest --run", "test:integration": "HOLO_CLI_INCLUDE_INTEGRATION=1 vitest --run tests/cli.test.ts" diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 6a898a0..d0eed4f 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -494,16 +494,15 @@ export function createInternalCommands( installEventsIntoProject, installQueueIntoProject, } = await loadProjectScaffoldModule() - const eventsResult = await installEventsIntoProject(context.projectRoot) - let queueResult: - | Awaited> - | undefined - const queueConfigured = await hasProjectDependency(context.projectRoot, '@holo-js/queue') || await fileExists(resolve(context.projectRoot, 'config/queue.ts')) || await fileExists(resolve(context.projectRoot, 'config/queue.mts')) || await fileExists(resolve(context.projectRoot, 'config/queue.js')) || await fileExists(resolve(context.projectRoot, 'config/queue.mjs')) + const eventsResult = await installEventsIntoProject(context.projectRoot) + let queueResult: + | Awaited> + | undefined if ( !queueConfigured diff --git a/packages/cli/src/generated/workspaceCatalog.ts b/packages/cli/src/generated/workspaceCatalog.ts new file mode 100644 index 0000000..31b50b6 --- /dev/null +++ b/packages/cli/src/generated/workspaceCatalog.ts @@ -0,0 +1,43 @@ +// Generated by scripts/generate-cli-workspace-catalog.mjs. Do not edit by hand. + +export const WORKSPACE_CATALOG = Object.freeze({ + "better-sqlite3": "^11.7.0", + "@types/better-sqlite3": "^7.6.12", + "nuxt": "^4.4.4", + "vue": "^3.5.13", + "vue-router": "^5.0.4", + "vite": "^8.0.10", + "svelte": "^5.55.5", + "next": "^16.2.4", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@sveltejs/kit": "^2.59.1", + "@sveltejs/vite-plugin-svelte": "^7.1.0", + "@sveltejs/adapter-node": "^5.5.4", + "@nuxt/kit": "^4.4.4", + "@nuxt/module-builder": "^1.0.2", + "@eslint/js": "^9.17.0", + "@types/node": "^22.10.2", + "esbuild": "^0.27.4", + "globals": "^15.14.0", + "@types/pg": "^8.11.0", + "inflection": "^3.0.2", + "mysql2": "^3.17.1", + "pg": "^8.13.0", + "ioredis": "^5.4.2", + "tslib": "^2.8.1", + "typescript-eslint": "^8.30.1", + "typescript": "^5.7.2", + "vue-tsc": "^2.2.0", + "vitepress": "^1.6.3", + "vitest": "^4.1.5", + "@vitest/coverage-istanbul": "^4.1.5", + "@vitest/coverage-v8": "^4.1.5", + "tsup": "^8.3.5", + "eslint": "^9.17.0", + "fast-check": "^4.5.3", + "ulid": "^3.0.1", + "uuid": "^12.0.0" +} as const) diff --git a/packages/cli/src/generators.ts b/packages/cli/src/generators.ts index 680818f..8ff0ed9 100644 --- a/packages/cli/src/generators.ts +++ b/packages/cli/src/generators.ts @@ -1,6 +1,6 @@ import { createHash } from 'node:crypto' import { readFile } from 'node:fs/promises' -import { resolve } from 'node:path' +import { basename, extname, resolve } from 'node:path' import { normalizeMigrationSlug } from '@holo-js/db' import { ensureGeneratedSchemaPlaceholder, @@ -41,6 +41,7 @@ import { nextMigrationTemplate, } from './migrations' import { writeLine } from './io' +import { collectFiles } from './project/discovery-helpers' import type { IoStreams, PreparedInput } from './cli-types' type MailTemplateType = 'markdown' | 'view' @@ -55,6 +56,145 @@ export function hasRegisteredModelName( return Boolean(registry?.models.some(entry => entry.name === modelName)) } +function findRegisteredModelByTableName( + registry: Awaited> | undefined, + tableName: string, +) { + return registry?.models.find(entry => entry.tableName === tableName) +} + +async function findGeneratedModelSourceByTableName( + projectRoot: string, + modelsPath: string, + tableName: string, +): Promise { + const files = await collectFiles(resolve(projectRoot, modelsPath)) + const generatedTableReference = tableName + + for (const filePath of files) { + const contents = await readFile(filePath, 'utf8') + if (containsDefineModelTableReference(contents, generatedTableReference)) { + return basename(filePath, extname(filePath)) + } + } + + return undefined +} + +function containsDefineModelTableReference(contents: string, tableName: string): boolean { + let index = 0 + + while (index < contents.length) { + const nextReference = contents.indexOf('defineModel', index) + if (nextReference === -1) return false + if (isInsideComment(contents, nextReference)) { + index = nextReference + 'defineModel'.length + continue + } + + const before = contents[nextReference - 1] + const after = contents[nextReference + 'defineModel'.length] + const hasIdentifierBoundary = !isIdentifierCharacter(before) && !isIdentifierCharacter(after) + if (!hasIdentifierBoundary) { + index = nextReference + 'defineModel'.length + continue + } + + const openParenIndex = skipWhitespace(contents, nextReference + 'defineModel'.length) + if (contents[openParenIndex] !== '(') { + index = nextReference + 'defineModel'.length + continue + } + + const firstArgumentIndex = skipWhitespace(contents, openParenIndex + 1) + const quote = contents[firstArgumentIndex] + if (quote !== '\'' && quote !== '"' && quote !== '`') { + index = firstArgumentIndex + continue + } + + const literal = readStringLiteral(contents, firstArgumentIndex, quote) + if (literal?.value === tableName) return true + index = literal?.endIndex ?? firstArgumentIndex + 1 + } + + return false +} + +function isInsideComment(contents: string, position: number): boolean { + let index = 0 + while (index < position) { + const current = contents[index] + const next = contents[index + 1] + + if (current === '\'' || current === '"' || current === '`') { + index = readStringLiteral(contents, index, current)?.endIndex ?? index + 1 + continue + } + + if (current === '/' && next === '/') { + const end = contents.indexOf('\n', index + 2) + if (end === -1 || end >= position) return true + index = end + 1 + continue + } + + if (current === '/' && next === '*') { + const end = contents.indexOf('*/', index + 2) + if (end === -1 || end + 2 >= position) return true + index = end + 2 + continue + } + + index += 1 + } + + return false +} + +function isIdentifierCharacter(value: string | undefined): boolean { + return typeof value === 'string' && /[$\w]/.test(value) +} + +function skipWhitespace(contents: string, startIndex: number): number { + let index = startIndex + while (/\s/.test(contents[index] ?? '')) { + index += 1 + } + return index +} + +function readStringLiteral( + contents: string, + startIndex: number, + quote: '\'' | '"' | '`', +): { readonly value: string, readonly endIndex: number } | undefined { + let value = '' + let index = startIndex + 1 + while (index < contents.length) { + const current = contents[index] + if (current === '\\') { + const escaped = contents[index + 1] + if (typeof escaped === 'string') { + value += escaped + index += 2 + continue + } + } + + if (current === quote) { + return { value, endIndex: index + 1 } + } + + if (typeof current === 'string') { + value += current + } + index += 1 + } + + return undefined +} + export function hasRegisteredJobName( registry: Awaited> | undefined, jobName: string, @@ -180,6 +320,7 @@ export async function runMakeModel( input: PreparedInput, ): Promise { const project = await ensureProjectConfig(projectRoot) + const generatedSchemaFilePath = await ensureGeneratedSchemaPlaceholder(projectRoot, project.config) const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, project.config) /* v8 ignore next */ @@ -200,7 +341,6 @@ export async function runMakeModel( const seederFilePath = resolveArtifactPath(projectRoot, project.config.paths.seeders, seederInfo.directory, `${seederInfo.baseName}.ts`) const factoryInfo = resolveNameInfo(`${requestedName}Factory`, { suffix: 'Factory' }) const factoryFilePath = resolveArtifactPath(projectRoot, project.config.paths.factories, factoryInfo.directory, `${factoryInfo.baseName}.ts`) - const generatedSchemaFilePath = await ensureGeneratedSchemaPlaceholder(projectRoot, project.config) if (await fileExists(modelFilePath) || hasRegisteredModelName(registry, nameInfo.baseName)) { throw new Error(`Model with the same name already exists: ${nameInfo.baseName}.`) @@ -213,6 +353,20 @@ export async function runMakeModel( } } + const existingTableModel = findRegisteredModelByTableName(registry, tableName) + if (existingTableModel) { + throw new Error(`Discovered duplicate model "${existingTableModel.name}" for table "${tableName}".`) + } + + const existingGeneratedModelName = await findGeneratedModelSourceByTableName( + projectRoot, + project.config.paths.models, + tableName, + ) + if (existingGeneratedModelName) { + throw new Error(`Discovered duplicate model "${existingGeneratedModelName}" for table "${tableName}".`) + } + await ensureAbsent(modelFilePath) if (options.observer) { await ensureAbsent(observerFilePath) diff --git a/packages/cli/src/metadata.ts b/packages/cli/src/metadata.ts index 202640e..198927d 100644 --- a/packages/cli/src/metadata.ts +++ b/packages/cli/src/metadata.ts @@ -1,9 +1,17 @@ import packageJson from '../package.json' with { type: 'json' } +import { WORKSPACE_CATALOG } from './generated/workspaceCatalog' export const HOLO_PACKAGE_VERSION = packageJson.version -export const ESBUILD_PACKAGE_VERSION = '^0.27.4' const HOLO_PACKAGE_RANGE = `^${HOLO_PACKAGE_VERSION}` +function catalogVersion( + packageName: TPackageName, +): (typeof WORKSPACE_CATALOG)[TPackageName] { + return WORKSPACE_CATALOG[packageName] +} + +export const ESBUILD_PACKAGE_VERSION = catalogVersion('esbuild') + export const SCAFFOLD_PACKAGE_MANAGER_VERSIONS = Object.freeze({ npm: 'npm@latest', pnpm: 'pnpm@latest', @@ -12,11 +20,39 @@ export const SCAFFOLD_PACKAGE_MANAGER_VERSIONS = Object.freeze({ } as const) export const SCAFFOLD_FRAMEWORK_VERSIONS = Object.freeze({ - nuxt: '^4.4.4', - next: '^16.2.4', - sveltekit: '^2.59.1', + nuxt: catalogVersion('nuxt'), + next: catalogVersion('next'), + sveltekit: catalogVersion('@sveltejs/kit'), +} as const) + +export const SCAFFOLD_NEXT_REACT_VERSIONS = Object.freeze({ + react: catalogVersion('react'), + 'react-dom': catalogVersion('react-dom'), + '@types/react': catalogVersion('@types/react'), + '@types/react-dom': catalogVersion('@types/react-dom'), +} as const) + +export const SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS = Object.freeze({ + typescript: catalogVersion('typescript'), + '@types/node': catalogVersion('@types/node'), + eslint: catalogVersion('eslint'), } as const) +export const SCAFFOLD_NUXT_DEPENDENCY_VERSIONS = Object.freeze({ + vue: catalogVersion('vue'), + 'vue-router': catalogVersion('vue-router'), + 'vue-tsc': catalogVersion('vue-tsc'), +} as const) + +export const SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS = Object.freeze({ + '@sveltejs/adapter-node': catalogVersion('@sveltejs/adapter-node'), + '@sveltejs/vite-plugin-svelte': catalogVersion('@sveltejs/vite-plugin-svelte'), + svelte: catalogVersion('svelte'), + vite: catalogVersion('vite'), +} as const) + +export const IOREDIS_PACKAGE_VERSION = catalogVersion('ioredis') + export const SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS = Object.freeze({ nuxt: HOLO_PACKAGE_RANGE, next: HOLO_PACKAGE_RANGE, diff --git a/packages/cli/src/project/discovery-helpers.ts b/packages/cli/src/project/discovery-helpers.ts index c510eab..489a105 100644 --- a/packages/cli/src/project/discovery-helpers.ts +++ b/packages/cli/src/project/discovery-helpers.ts @@ -254,6 +254,8 @@ export function isCliModelReference(value: unknown): value is CliModelReference && isRecord(value.definition) && value.definition.kind === 'model' && typeof value.definition.name === 'string' + && isRecord(value.definition.table) + && typeof value.definition.table.tableName === 'string' && typeof value.prune === 'function' } diff --git a/packages/cli/src/project/registry-svelte.ts b/packages/cli/src/project/registry-svelte.ts index daed136..5120ef9 100644 --- a/packages/cli/src/project/registry-svelte.ts +++ b/packages/cli/src/project/registry-svelte.ts @@ -1,5 +1,5 @@ -import { readFile, unlink, writeFile } from 'node:fs/promises' -import { resolve } from 'node:path' +import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' import { GENERATED_SVELTE_HOOKS_PATH, GENERATED_SVELTE_SERVER_HOOKS_PATH, @@ -234,6 +234,7 @@ async function writeFileIfChanged(path: string, contents: string): Promise return } + await mkdir(dirname(path), { recursive: true }) await writeFile(path, contents, 'utf8') } diff --git a/packages/cli/src/project/registry.ts b/packages/cli/src/project/registry.ts index e195d2a..2b790d0 100644 --- a/packages/cli/src/project/registry.ts +++ b/packages/cli/src/project/registry.ts @@ -1,4 +1,5 @@ -import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises' +import { constants as fsConstants } from 'node:fs' +import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises' import { dirname, extname, join, resolve } from 'node:path' import { loadConfigDirectory } from '@holo-js/config' import { DEFAULT_HOLO_PROJECT_PATHS, renderGeneratedSchemaRuntimeModule, type TableDefinition } from '@holo-js/db' @@ -79,11 +80,27 @@ function extractGeneratedSchemaTables(moduleValue: unknown): readonly TableDefin return [] } +function getFilesystemErrorCode(error: unknown): string | undefined { + return error instanceof Error && 'code' in error && typeof error.code === 'string' + ? error.code + : undefined +} + async function renderGeneratedSchemaRuntimeArtifact( projectRoot: string, schemaEntry: string, ): Promise { const schemaPath = resolve(projectRoot, schemaEntry) + try { + await access(schemaPath, fsConstants.R_OK) + } catch (error) { + const code = getFilesystemErrorCode(error) + if (code === 'ENOENT' || code === 'ENOTDIR') { + return renderGeneratedSchemaRuntimeModule([]) + } + throw error + } + const moduleValue = await importProjectModule(projectRoot, schemaPath) const tables = extractGeneratedSchemaTables(moduleValue) return renderGeneratedSchemaRuntimeModule(tables) @@ -821,17 +838,27 @@ export async function loadGeneratedProjectRegistry( ): Promise { const filePath = resolve(projectRoot, GENERATED_REGISTRY_JSON_PATH) const contents = await readFile(filePath, 'utf8').catch(() => undefined) - if (!contents) { - return undefined - } - try { - const parsed = JSON.parse(contents) as unknown - if (isGeneratedProjectRegistry(parsed)) { - return parsed + if (contents) { + try { + const parsed = JSON.parse(contents) as unknown + if (isGeneratedProjectRegistry(parsed)) { + return parsed + } + } catch { + // Fall through to the generated module fallback below. } - } catch { - return undefined + } + + const generatedIndexPath = resolve(projectRoot, GENERATED_INDEX_PATH) + const generatedModule = await importProjectModule(projectRoot, generatedIndexPath).catch(() => undefined) + + if (isRecord(generatedModule) && isGeneratedProjectRegistry(generatedModule.registry)) { + return generatedModule.registry + } + + if (isRecord(generatedModule) && isGeneratedProjectRegistry(generatedModule.default)) { + return generatedModule.default } return undefined diff --git a/packages/cli/src/project/scaffold/dependencies.ts b/packages/cli/src/project/scaffold/dependencies.ts index 8fba2db..532490a 100644 --- a/packages/cli/src/project/scaffold/dependencies.ts +++ b/packages/cli/src/project/scaffold/dependencies.ts @@ -3,6 +3,7 @@ import { loadConfigDirectory, type SupportedDatabaseDriver } from '@holo-js/conf import { ESBUILD_PACKAGE_VERSION, HOLO_PACKAGE_VERSION, + IOREDIS_PACKAGE_VERSION, } from '../../metadata' import { loadProjectConfig } from '../config' import { @@ -23,8 +24,6 @@ import { import { loadGeneratedProjectRegistry } from '../registry' import type { LoadedConfigWithCache } from './types' -const IOREDIS_PACKAGE_VERSION = '^5.4.2' - function normalizeDependencyMap(value: unknown): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { return {} @@ -37,6 +36,31 @@ function normalizeDependencyMap(value: unknown): Record { ) } +function isWorkspaceDependencyVersion(value: string | undefined): value is string { + return typeof value === 'string' && value.startsWith('workspace:') +} + +function resolveManagedHoloPackageVersion( + packageName: string, + dependencies: Record, + devDependencies: Record, +): string { + const currentPackageVersion = dependencies[packageName] ?? devDependencies[packageName] + if (isWorkspaceDependencyVersion(currentPackageVersion)) { + return currentPackageVersion + } + + const workspaceVersion = Object.entries({ + ...dependencies, + ...devDependencies, + }).find(([dependencyName, dependencyVersion]) => ( + dependencyName.startsWith('@holo-js/') + && isWorkspaceDependencyVersion(dependencyVersion) + ))?.[1] + + return workspaceVersion ?? `^${HOLO_PACKAGE_VERSION}` +} + export async function readPackageJsonDependencyState(projectRoot: string): Promise<{ packageJsonPath: string parsed: Record @@ -246,7 +270,7 @@ export async function syncManagedDriverDependencies( } = await readPackageJsonDependencyState(projectRoot) const cachePackageInstalled = typeof dependencies['@holo-js/cache'] !== 'undefined' || typeof devDependencies['@holo-js/cache'] !== 'undefined' - const cacheDesired = cacheConfigured + const cacheDesired = cacheConfigured || cachePackageInstalled requiredPackages.add('@holo-js/core') @@ -364,7 +388,6 @@ 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), @@ -397,7 +420,7 @@ export async function syncManagedDriverDependencies( for (const packageName of requiredPackages) { const requiredVersion = packageName === 'ioredis' ? IOREDIS_PACKAGE_VERSION - : nextVersion + : resolveManagedHoloPackageVersion(packageName, dependencies, devDependencies) if (dependencies[packageName] !== requiredVersion || typeof devDependencies[packageName] !== 'undefined') { dependencies[packageName] = requiredVersion delete devDependencies[packageName] @@ -438,7 +461,9 @@ async function upsertQueuePackageDependency( }).then(config => config.queue) .catch(() => undefined) : Promise.resolve(undefined) - const nextVersion = `^${HOLO_PACKAGE_VERSION}` + const nextVersion = resolveManagedHoloPackageVersion('@holo-js/queue', dependencies, devDependencies) + const nextQueueDbVersion = resolveManagedHoloPackageVersion('@holo-js/queue-db', dependencies, devDependencies) + const nextQueueRedisVersion = resolveManagedHoloPackageVersion('@holo-js/queue-redis', dependencies, devDependencies) const nextEsbuildVersion = ESBUILD_PACKAGE_VERSION const queueConfig = typeof driver === 'undefined' ? await loadedQueueConfig @@ -462,8 +487,8 @@ async function upsertQueuePackageDependency( if ( currentVersion === nextVersion - && (requiresQueueDb ? currentQueueDbVersion === nextVersion : typeof currentQueueDbVersion === 'undefined') - && (requiresQueueRedis ? currentQueueRedisVersion === nextVersion : typeof currentQueueRedisVersion === 'undefined') + && (requiresQueueDb ? currentQueueDbVersion === nextQueueDbVersion : typeof currentQueueDbVersion === 'undefined') + && (requiresQueueRedis ? currentQueueRedisVersion === nextQueueRedisVersion : typeof currentQueueRedisVersion === 'undefined') && typeof currentDevVersion === 'undefined' && typeof currentDevQueueDbVersion === 'undefined' && typeof currentDevQueueRedisVersion === 'undefined' @@ -475,12 +500,12 @@ async function upsertQueuePackageDependency( dependencies['@holo-js/queue'] = nextVersion if (requiresQueueDb) { - dependencies['@holo-js/queue-db'] = nextVersion + dependencies['@holo-js/queue-db'] = nextQueueDbVersion } else { delete dependencies['@holo-js/queue-db'] } if (requiresQueueRedis) { - dependencies['@holo-js/queue-redis'] = nextVersion + dependencies['@holo-js/queue-redis'] = nextQueueRedisVersion } else { delete dependencies['@holo-js/queue-redis'] } @@ -496,7 +521,8 @@ async function upsertQueuePackageDependency( async function upsertEventsPackageDependency(projectRoot: string): Promise { const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot) - const nextVersion = `^${HOLO_PACKAGE_VERSION}` + const nextVersion = resolveManagedHoloPackageVersion('@holo-js/events', dependencies, devDependencies) + const nextQueueVersion = resolveManagedHoloPackageVersion('@holo-js/queue', dependencies, devDependencies) const currentVersion = dependencies['@holo-js/events'] const currentDevVersion = devDependencies['@holo-js/events'] const currentQueueVersion = dependencies['@holo-js/queue'] @@ -505,14 +531,14 @@ async function upsertEventsPackageDependency(projectRoot: string): Promise { const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot) - const nextVersion = `^${HOLO_PACKAGE_VERSION}` + const nextVersion = resolveManagedHoloPackageVersion('@holo-js/notifications', dependencies, devDependencies) const currentVersion = dependencies['@holo-js/notifications'] const currentDevVersion = devDependencies['@holo-js/notifications'] @@ -538,7 +564,7 @@ async function upsertNotificationsPackageDependency(projectRoot: string): Promis async function upsertMailPackageDependency(projectRoot: string): Promise { const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot) - const nextVersion = `^${HOLO_PACKAGE_VERSION}` + const nextVersion = resolveManagedHoloPackageVersion('@holo-js/mail', dependencies, devDependencies) const currentVersion = dependencies['@holo-js/mail'] const currentDevVersion = devDependencies['@holo-js/mail'] @@ -554,7 +580,7 @@ async function upsertMailPackageDependency(projectRoot: string): Promise { const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot) - const nextVersion = `^${HOLO_PACKAGE_VERSION}` + const nextVersion = resolveManagedHoloPackageVersion('@holo-js/security', dependencies, devDependencies) const currentVersion = dependencies['@holo-js/security'] const currentDevVersion = devDependencies['@holo-js/security'] @@ -581,7 +607,9 @@ async function upsertCachePackageDependencies( }).then(config => (config as LoadedConfigWithCache).cache) .catch(() => undefined) : undefined - const nextVersion = `^${HOLO_PACKAGE_VERSION}` + const nextVersion = resolveManagedHoloPackageVersion('@holo-js/cache', dependencies, devDependencies) + const nextCacheDbVersion = resolveManagedHoloPackageVersion('@holo-js/cache-db', dependencies, devDependencies) + const nextCacheRedisVersion = resolveManagedHoloPackageVersion('@holo-js/cache-redis', dependencies, devDependencies) const requiresCacheRedis = driver === 'redis' || Object.values(cacheConfig?.drivers ?? {}).some(connection => connection.driver === 'redis') const requiresCacheDb = driver === 'database' @@ -595,8 +623,8 @@ async function upsertCachePackageDependencies( if ( currentVersion === nextVersion - && (requiresCacheDb ? currentCacheDbVersion === nextVersion : typeof currentCacheDbVersion === 'undefined') - && (requiresCacheRedis ? currentCacheRedisVersion === nextVersion : typeof currentCacheRedisVersion === 'undefined') + && (requiresCacheDb ? currentCacheDbVersion === nextCacheDbVersion : typeof currentCacheDbVersion === 'undefined') + && (requiresCacheRedis ? currentCacheRedisVersion === nextCacheRedisVersion : typeof currentCacheRedisVersion === 'undefined') && typeof currentDevVersion === 'undefined' && typeof currentDevCacheDbVersion === 'undefined' && typeof currentDevCacheRedisVersion === 'undefined' @@ -606,12 +634,12 @@ async function upsertCachePackageDependencies( dependencies['@holo-js/cache'] = nextVersion if (requiresCacheDb) { - dependencies['@holo-js/cache-db'] = nextVersion + dependencies['@holo-js/cache-db'] = nextCacheDbVersion } else { delete dependencies['@holo-js/cache-db'] } if (requiresCacheRedis) { - dependencies['@holo-js/cache-redis'] = nextVersion + dependencies['@holo-js/cache-redis'] = nextCacheRedisVersion } else { delete dependencies['@holo-js/cache-redis'] } @@ -647,7 +675,7 @@ export async function upsertBroadcastPackageDependencies(projectRoot: string): P readonly framework: 'next' | 'nuxt' | 'sveltekit' | undefined }> { const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot) - const nextVersion = `^${HOLO_PACKAGE_VERSION}` + const nextVersion = resolveManagedHoloPackageVersion('@holo-js/broadcast', dependencies, devDependencies) const framework = detectProjectFrameworkFromPackageJson(dependencies, devDependencies) let changed = false @@ -722,7 +750,6 @@ async function upsertAuthPackageDependencies( } = {}, ): Promise { const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot) - const nextVersion = `^${HOLO_PACKAGE_VERSION}` const socialEnabled = features.social === true || (features.socialProviders?.length ?? 0) > 0 const requestedPackages = { '@holo-js/auth': true, @@ -739,6 +766,7 @@ async function upsertAuthPackageDependencies( for (const [packageName, enabled] of Object.entries(requestedPackages)) { const currentDependency = dependencies[packageName] const currentDevDependency = devDependencies[packageName] + const nextVersion = resolveManagedHoloPackageVersion(packageName, dependencies, devDependencies) if (enabled) { if (currentDependency !== nextVersion || typeof currentDevDependency !== 'undefined') { @@ -764,6 +792,7 @@ async function upsertAuthPackageDependencies( const enabled = requestedSocialProviders.has(providerName as SupportedAuthSocialProvider) const currentDependency = dependencies[packageName] const currentDevDependency = devDependencies[packageName] + const nextVersion = resolveManagedHoloPackageVersion(packageName, dependencies, devDependencies) if (enabled) { if (currentDependency !== nextVersion || typeof currentDevDependency !== 'undefined') { @@ -795,7 +824,7 @@ async function upsertAuthPackageDependencies( async function upsertAuthorizationPackageDependency(projectRoot: string): Promise { const { packageJsonPath, parsed, dependencies, devDependencies } = await readPackageJsonDependencyState(projectRoot) - const nextVersion = `^${HOLO_PACKAGE_VERSION}` + const nextVersion = resolveManagedHoloPackageVersion('@holo-js/authorization', dependencies, devDependencies) const currentVersion = dependencies['@holo-js/authorization'] const currentDevVersion = devDependencies['@holo-js/authorization'] diff --git a/packages/cli/src/project/scaffold/framework-renderers.ts b/packages/cli/src/project/scaffold/framework-renderers.ts index 3882395..dcc352b 100644 --- a/packages/cli/src/project/scaffold/framework-renderers.ts +++ b/packages/cli/src/project/scaffold/framework-renderers.ts @@ -513,7 +513,7 @@ export function renderFrameworkFiles(options: ProjectScaffoldOptions): readonly export function renderFrameworkRunner(options: Pick): string { const commandName = options.framework === 'nuxt' - ? 'nuxi' + ? 'nuxt' : options.framework === 'next' ? 'next' : 'vite' diff --git a/packages/cli/src/project/scaffold/framework.ts b/packages/cli/src/project/scaffold/framework.ts index 2e83e35..cbed11a 100644 --- a/packages/cli/src/project/scaffold/framework.ts +++ b/packages/cli/src/project/scaffold/framework.ts @@ -7,10 +7,14 @@ import { import { ESBUILD_PACKAGE_VERSION, HOLO_PACKAGE_VERSION, + SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS, SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS, SCAFFOLD_FRAMEWORK_RUNTIME_VERSIONS, SCAFFOLD_FRAMEWORK_VERSIONS, + SCAFFOLD_NEXT_REACT_VERSIONS, + SCAFFOLD_NUXT_DEPENDENCY_VERSIONS, SCAFFOLD_PACKAGE_MANAGER_VERSIONS, + SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS, } from '../../metadata' import { resolveGeneratedSchemaPath } from '../config' import { renderGeneratedModelTypes } from '../registry' @@ -86,34 +90,35 @@ export function renderScaffoldPackageJson(options: ProjectScaffoldOptions): stri esbuild: ESBUILD_PACKAGE_VERSION, } const devDependencies: Record = { - typescript: '^5.8.0', - '@types/node': '^22.0.0', + typescript: SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS.typescript, + '@types/node': SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS['@types/node'], + eslint: SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS.eslint, } if (options.framework === 'nuxt') { dependencies.nuxt = SCAFFOLD_FRAMEWORK_VERSIONS.nuxt - dependencies.vue = '^3.5.13' - dependencies['vue-router'] = '^5.0.4' + dependencies.vue = SCAFFOLD_NUXT_DEPENDENCY_VERSIONS.vue + dependencies['vue-router'] = SCAFFOLD_NUXT_DEPENDENCY_VERSIONS['vue-router'] dependencies['@holo-js/adapter-nuxt'] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.nuxt - devDependencies['vue-tsc'] = '^2.2.0' + devDependencies['vue-tsc'] = SCAFFOLD_NUXT_DEPENDENCY_VERSIONS['vue-tsc'] } if (options.framework === 'next') { dependencies.next = SCAFFOLD_FRAMEWORK_VERSIONS.next - dependencies.react = '^19.0.0' - dependencies['react-dom'] = '^19.0.0' + dependencies.react = SCAFFOLD_NEXT_REACT_VERSIONS.react + dependencies['react-dom'] = SCAFFOLD_NEXT_REACT_VERSIONS['react-dom'] dependencies['@holo-js/adapter-next'] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.next - devDependencies['@types/react'] = '^19.0.0' - devDependencies['@types/react-dom'] = '^19.0.0' + devDependencies['@types/react'] = SCAFFOLD_NEXT_REACT_VERSIONS['@types/react'] + devDependencies['@types/react-dom'] = SCAFFOLD_NEXT_REACT_VERSIONS['@types/react-dom'] } if (options.framework === 'sveltekit') { dependencies['@holo-js/adapter-sveltekit'] = SCAFFOLD_FRAMEWORK_ADAPTER_VERSIONS.sveltekit - dependencies['@sveltejs/adapter-node'] = '^5.5.4' + dependencies['@sveltejs/adapter-node'] = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS['@sveltejs/adapter-node'] dependencies['@sveltejs/kit'] = SCAFFOLD_FRAMEWORK_VERSIONS.sveltekit - dependencies['@sveltejs/vite-plugin-svelte'] = '^7.1.0' - dependencies.svelte = '^5.55.5' - dependencies.vite = '^8.0.10' + dependencies['@sveltejs/vite-plugin-svelte'] = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS['@sveltejs/vite-plugin-svelte'] + dependencies.svelte = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS.svelte + dependencies.vite = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS.vite } if (optionalPackages.includes('storage')) { @@ -188,15 +193,15 @@ export function renderScaffoldPackageJson(options: ProjectScaffoldOptions): stri dev: 'holo dev', build: 'holo build', lint: options.framework === 'nuxt' - ? 'npx eslint app config server shared tests *.d.ts --fix --no-warn-ignored --no-error-on-unmatched-pattern' + ? 'eslint app config server shared tests *.d.ts --fix --no-warn-ignored --no-error-on-unmatched-pattern' : options.framework === 'next' - ? 'npx eslint app config server tests --fix --no-warn-ignored --no-error-on-unmatched-pattern' - : 'npx eslint src config server tests --fix --no-warn-ignored --no-error-on-unmatched-pattern', + ? 'eslint app config server tests --fix --no-warn-ignored --no-error-on-unmatched-pattern' + : 'eslint src config server tests --fix --no-warn-ignored --no-error-on-unmatched-pattern', typecheck: options.framework === 'nuxt' - ? 'npx nuxi typecheck' + ? 'nuxt typecheck' : options.framework === 'next' - ? 'npx tsc -p tsconfig.json --noEmit' - : 'npx svelte-kit sync && npx svelte-check --tsconfig ./tsconfig.json', + ? 'tsc -p tsconfig.json --noEmit' + : 'svelte-kit sync && svelte-check --tsconfig ./tsconfig.json', ['config:cache']: 'holo config:cache', ['config:clear']: 'holo config:clear', ['holo:dev']: 'node ./.holo-js/framework/run.mjs dev', diff --git a/packages/cli/tests/cli.test.ts b/packages/cli/tests/cli.test.ts index 33e440a..265f536 100644 --- a/packages/cli/tests/cli.test.ts +++ b/packages/cli/tests/cli.test.ts @@ -1,5 +1,6 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs' import { chmod, mkdtemp, mkdir, readFile, readdir, rm, stat, symlink, writeFile } from 'node:fs/promises' +import type * as FsPromisesModule from 'node:fs/promises' import { tmpdir } from 'node:os' import { basename, dirname, extname, join, resolve } from 'node:path' import { spawn, spawnSync } from 'node:child_process' @@ -54,6 +55,7 @@ import { writeProjectConfig, writeTextFile, } from '../src/project' +import { renderFrameworkAwareTsconfig, writeGeneratedProjectRegistry } from '../src/project/registry' import { ensureSuffix, pluralize, @@ -71,7 +73,15 @@ import { toPascalCase, toSnakeCase, } from '../src/templates' -import { ESBUILD_PACKAGE_VERSION, HOLO_PACKAGE_VERSION } from '../src/metadata' +import { + ESBUILD_PACKAGE_VERSION, + HOLO_PACKAGE_VERSION, + IOREDIS_PACKAGE_VERSION, + SCAFFOLD_FRAMEWORK_VERSIONS, + SCAFFOLD_NEXT_REACT_VERSIONS, + SCAFFOLD_NUXT_DEPENDENCY_VERSIONS, + SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS, +} from '../src/metadata' import type { FSWatcher } from 'node:fs' const workspaceRoot = resolve(import.meta.dirname, '../../..') @@ -100,6 +110,12 @@ type BuiltWorkspacePackages = { const tempBuildRoots: string[] = [] let builtWorkspacePackages: BuiltWorkspacePackages | null = null const expectedHoloPackageRange = `^${HOLO_PACKAGE_VERSION}` +const expectedNextPackageRange = SCAFFOLD_FRAMEWORK_VERSIONS.next +const expectedReactPackageRange = SCAFFOLD_NEXT_REACT_VERSIONS.react +const expectedNuxtPackageRange = SCAFFOLD_FRAMEWORK_VERSIONS.nuxt +const expectedSvelteKitPackageRange = SCAFFOLD_FRAMEWORK_VERSIONS.sveltekit +const expectedVueRouterPackageRange = SCAFFOLD_NUXT_DEPENDENCY_VERSIONS['vue-router'] +const expectedSvelteVitePluginPackageRange = SCAFFOLD_SVELTEKIT_DEPENDENCY_VERSIONS['@sveltejs/vite-plugin-svelte'] const outdatedHoloPackageRange = '^0.0.1' function createTempBuildRootSync(prefix: string): string { @@ -621,9 +637,9 @@ export default { ['holo:build']: 'node ./.holo-js/framework/run.mjs build', }, dependencies: { - 'next': '^16.0.0', - 'react': '^19.0.0', - 'react-dom': '^19.0.0', + 'next': expectedNextPackageRange, + 'react': expectedReactPackageRange, + 'react-dom': expectedReactPackageRange, '@holo-js/adapter-next': expectedHoloPackageRange, '@holo-js/cli': expectedHoloPackageRange, '@holo-js/config': expectedHoloPackageRange, @@ -1041,6 +1057,16 @@ export default { storageDefaultDisk: 'public', optionalPackages: ['storage', 'events', 'queue'], })).not.toContain(`"@holo-js/queue-db": "${expectedHoloPackageRange}"`) + expect(JSON.parse(projectInternals.renderScaffoldPackageJson({ + projectName: 'Linted App', + framework: 'next', + databaseDriver: 'sqlite', + packageManager: 'bun', + storageDefaultDisk: 'local', + optionalPackages: [], + })).devDependencies).toMatchObject({ + eslint: expect.any(String), + }) expect(projectInternals.renderScaffoldPackageJson({ projectName: 'Broadcast Runtime App', framework: 'next', @@ -1403,7 +1429,7 @@ export default { packageManager: 'bun', storageDefaultDisk: 'local', optionalPackages: [], - })).toContain('"@sveltejs/vite-plugin-svelte": "^7.1.0"') + })).toContain(`"@sveltejs/vite-plugin-svelte": "${expectedSvelteVitePluginPackageRange}"`) expect(projectInternals.renderScaffoldPackageJson({ projectName: 'Svelte App', framework: 'sveltekit', @@ -1419,7 +1445,7 @@ export default { packageManager: 'bun', storageDefaultDisk: 'local', optionalPackages: [], - })).toContain('"vue-router": "^5.0.4"') + })).toContain(`"vue-router": "${expectedVueRouterPackageRange}"`) expect(projectInternals.renderScaffoldPackageJson({ projectName: 'Svelte App', framework: 'sveltekit', @@ -1545,7 +1571,7 @@ export default { packageManager: 'bun', storageDefaultDisk: 'local', }) - await writeFrameworkBinary(nuxtRoot, 'nuxi') + await writeFrameworkBinary(nuxtRoot, 'nuxt') expect(runNodeScript(nuxtRoot, join(nuxtRoot, '.holo-js/framework/run.mjs'), ['dev']).stdout).toContain('dev') expect(await readFile(join(nuxtRoot, '.holo-js/framework/run.mjs'), 'utf8')).toContain("process.on('SIGTERM'") expect(await readFile(join(nuxtRoot, 'nuxt.config.ts'), 'utf8')).toContain('@holo-js/adapter-nuxt') @@ -1871,7 +1897,7 @@ export default defineAppConfig({ name: 'fixture', private: true, dependencies: { - next: '^16.0.0', + next: expectedNextPackageRange, }, }, null, 2)) @@ -2521,7 +2547,7 @@ export default defineAppConfig({ updatedEnvExample: false, }) await expect(projectInternals.installSecurityIntoProject(projectRoot)).resolves.toMatchObject({ - updatedPackageJson: true, + updatedPackageJson: false, createdSecurityConfig: true, }) await expect(projectInternals.installSecurityIntoProject(projectRoot)).resolves.toMatchObject({ @@ -2553,7 +2579,7 @@ export default defineAppConfig({ private: true, devDependencies: { '@holo-js/queue': outdatedHoloPackageRange, - 'typescript': '^5.0.0', + 'typescript': outdatedHoloPackageRange, }, optionalDependencies: ['ignored'], }, null, 2)) @@ -2584,7 +2610,7 @@ export default defineAppConfig({ '@holo-js/queue-redis': expectedHoloPackageRange, }, devDependencies: { - typescript: '^5.0.0', + typescript: outdatedHoloPackageRange, }, }) expect(JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8')).dependencies['@holo-js/queue-db']).toBeUndefined() @@ -2879,7 +2905,7 @@ export default defineRedisConfig({ private: true, devDependencies: { '@holo-js/events': outdatedHoloPackageRange, - typescript: '^5.0.0', + typescript: outdatedHoloPackageRange, }, }, null, 2)) await expect(projectInternals.installEventsIntoProject(eventsDevDependencyRoot)).resolves.toEqual({ @@ -2892,7 +2918,7 @@ export default defineRedisConfig({ '@holo-js/events': expectedHoloPackageRange, }, devDependencies: { - typescript: '^5.0.0', + typescript: outdatedHoloPackageRange, }, }) @@ -3143,6 +3169,78 @@ export default defineDatabaseConfig({ expect(JSON.parse(await readFile(join(staleQueuePackagesRoot, 'package.json'), 'utf8')).dependencies['@holo-js/queue-redis']).toBeUndefined() }, 30000) + it('preserves workspace versions when syncing managed dependencies in workspace apps', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + await writeProjectFile(projectRoot, 'package.json', JSON.stringify({ + name: 'fixture', + private: true, + dependencies: { + '@holo-js/auth': 'workspace:*', + '@holo-js/cache': 'workspace:*', + '@holo-js/db': 'workspace:*', + '@holo-js/db-sqlite': 'workspace:*', + }, + }, null, 2)) + await writeProjectFile(projectRoot, 'config/database.ts', ` +import { defineDatabaseConfig } from '@holo-js/config' + +export default defineDatabaseConfig({ + connections: { + default: { + driver: 'sqlite', + url: ':memory:', + }, + }, +}) +`) + await writeProjectFile(projectRoot, 'config/auth.ts', ` +import { defineAuthConfig } from '@holo-js/config' + +export default defineAuthConfig({ + guards: { + web: { + driver: 'session', + provider: 'users', + }, + }, + providers: { + users: { + model: 'User', + }, + }, +}) +`) + await writeProjectFile(projectRoot, 'config/cache.ts', ` +import { defineCacheConfig } from '@holo-js/config' + +export default defineCacheConfig({ + default: 'database', + drivers: { + database: { + driver: 'database', + connection: 'default', + table: 'cache', + }, + }, +}) +`) + + await expect(projectInternals.syncManagedDriverDependencies(projectRoot)).resolves.toBe(true) + expect(JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8'))).toMatchObject({ + dependencies: { + '@holo-js/auth': 'workspace:*', + '@holo-js/cache': 'workspace:*', + '@holo-js/cache-db': 'workspace:*', + '@holo-js/core': 'workspace:*', + '@holo-js/db': 'workspace:*', + '@holo-js/db-sqlite': 'workspace:*', + '@holo-js/security': 'workspace:*', + '@holo-js/session': 'workspace:*', + }, + }) + }, 30000) + it('syncs lazy optional holo packages from config and discovery registry entries', async () => { const projectRoot = await createTempProject() tempDirs.push(projectRoot) @@ -3329,7 +3427,7 @@ export default defineRedisConfig({ '@holo-js/queue': expectedHoloPackageRange, '@holo-js/security': expectedHoloPackageRange, '@holo-js/session': expectedHoloPackageRange, - 'ioredis': '^5.4.2', + 'ioredis': IOREDIS_PACKAGE_VERSION, }, }) expect(JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8')).dependencies['@holo-js/storage']).toBeUndefined() @@ -3416,7 +3514,7 @@ export default defineRedisConfig({ '@holo-js/security': expectedHoloPackageRange, '@holo-js/session': expectedHoloPackageRange, '@holo-js/storage': expectedHoloPackageRange, - 'ioredis': '^5.4.2', + 'ioredis': IOREDIS_PACKAGE_VERSION, }, }, null, 2)) await writeProjectFile(projectRoot, 'config/database.ts', ` @@ -4051,7 +4149,7 @@ export default defineRedisConfig({ name: 'next-broadcast-fixture', private: true, dependencies: { - next: '^16.0.0', + next: expectedNextPackageRange, }, }, null, 2)) await writeProjectFile(nextRoot, 'app/layout.tsx', 'export default function Layout({ children }: { children: React.ReactNode }) { return {children} }\n') @@ -4105,7 +4203,7 @@ await expect(readFile(join(nextRoot, 'config/broadcast.ts'), 'utf8')).resolves.t name: 'nuxt-broadcast-fixture', private: true, dependencies: { - nuxt: '^4.0.0', + nuxt: expectedNuxtPackageRange, }, }, null, 2)) const nuxtResult = runCliProcess(nuxtRoot, ['install', 'broadcast']) @@ -4123,7 +4221,7 @@ await expect(readFile(join(nextRoot, 'config/broadcast.ts'), 'utf8')).resolves.t name: 'svelte-broadcast-fixture', private: true, dependencies: { - '@sveltejs/kit': '^2.0.0', + '@sveltejs/kit': expectedSvelteKitPackageRange, }, }, null, 2)) const svelteResult = runCliProcess(svelteRoot, ['install', 'broadcast']) @@ -4167,7 +4265,7 @@ module.exports = { name: 'next-direct-broadcast-fixture', private: true, dependencies: { - next: '^16.0.0', + next: expectedNextPackageRange, }, }, null, 2)) await writeProjectFile(nextRoot, 'config/auth.ts', 'export default {}\n') @@ -4189,7 +4287,7 @@ module.exports = { name: 'nuxt-direct-broadcast-fixture', private: true, devDependencies: { - nuxt: '^4.0.0', + nuxt: expectedNuxtPackageRange, }, }, null, 2)) await writeProjectFile(nuxtRoot, 'config/auth.ts', 'export default {}\n') @@ -4211,7 +4309,7 @@ module.exports = { name: 'svelte-direct-broadcast-fixture', private: true, dependencies: { - '@sveltejs/kit': '^2.0.0', + '@sveltejs/kit': expectedSvelteKitPackageRange, }, }, null, 2)) await writeProjectFile(svelteRoot, 'config/auth.ts', 'export default {}\n') @@ -4280,7 +4378,7 @@ module.exports = { name: 'next-existing-broadcast-auth-fixture', private: true, dependencies: { - next: '^16.0.0', + next: expectedNextPackageRange, }, }, null, 2)) await writeProjectFile(nextRoot, 'config/auth.ts', 'export default {}\n') @@ -4330,7 +4428,7 @@ export default defineBroadcastConfig({ name: 'next-broadcast-auth-order-fixture', private: true, dependencies: { - next: '^16.0.0', + next: expectedNextPackageRange, }, }, null, 2)) @@ -4357,7 +4455,7 @@ export default defineBroadcastConfig({ name: 'next-broadcast-auth-formatted-fixture', private: true, dependencies: { - next: '^16.0.0', + next: expectedNextPackageRange, }, }, null, 2)) @@ -4480,10 +4578,10 @@ export default defineBroadcastConfig({ const io = createIo(baseRoot, { tty: true }) await expect(cliInternals.resolveNewProjectInput(io.io, { args: [], flags: {} }, { prompt: async () => 'prompted-app', - choose: async (_label, _allowed, defaultValue) => { - if (defaultValue === 'nuxt') return 'sveltekit' as typeof defaultValue - if (defaultValue === 'sqlite') return 'sqlite' as typeof defaultValue - if (defaultValue === 'bun') return 'yarn' as typeof defaultValue + choose: async (label, _allowed, defaultValue) => { + if (label === 'Framework') return 'sveltekit' as typeof defaultValue + if (label === 'Database driver') return 'sqlite' as typeof defaultValue + if (label === 'Package manager') return 'yarn' as typeof defaultValue return 'local' as typeof defaultValue }, optionalPackages: async () => ['validation'], @@ -4498,10 +4596,10 @@ export default defineBroadcastConfig({ await expect(cliInternals.resolveNewProjectInput(io.io, { args: [], flags: {} }, { prompt: async () => 'storage-app', - choose: async (_label, _allowed, defaultValue) => { - if (defaultValue === 'nuxt') return 'nuxt' as typeof defaultValue - if (defaultValue === 'sqlite') return 'sqlite' as typeof defaultValue - if (defaultValue === 'bun') return 'bun' as typeof defaultValue + choose: async (label, _allowed, defaultValue) => { + if (label === 'Framework') return 'nuxt' as typeof defaultValue + if (label === 'Database driver') return 'sqlite' as typeof defaultValue + if (label === 'Package manager') return 'bun' as typeof defaultValue return 'public' as typeof defaultValue }, optionalPackages: async () => ['storage'], @@ -4610,6 +4708,7 @@ export default defineBroadcastConfig({ const projectRoot = join(baseRoot, 'scripted-app') const scaffolded = runCliProcess(baseRoot, ['new', 'scripted-app']) expect(scaffolded.status).toBe(0) + await writeFrameworkBinary(projectRoot, 'nuxt') const optionalCliResult = runCliProcess(baseRoot, [ 'new', @@ -4655,8 +4754,6 @@ export default defineBroadcastConfig({ }) expect(await readFile(join(projectRoot, '.holo-js/generated/registry.json'), 'utf8')).toContain('"version": 1') - await writeFrameworkBinary(projectRoot, 'nuxi') - const devResult = runNodeScript(projectRoot, join(projectRoot, '.holo-js/framework/run.mjs'), ['dev']) expect(devResult.status, devResult.stderr || devResult.stdout).toBe(0) expect(devResult.stdout).toContain('dev') @@ -4964,6 +5061,46 @@ export default defineAppConfig({ }, 30000) + it('detects duplicate generated model tables from source literals without matching comments', async () => { + const duplicateRoot = await createTempProject() + tempDirs.push(duplicateRoot) + await linkWorkspaceDb(duplicateRoot) + await ensureGeneratedSchemaPlaceholder(duplicateRoot, defaultProjectConfig()) + await writeProjectFile(duplicateRoot, 'server/models/LegacyPerson.ts', ` +import '../../.holo-js/generated/schema.generated' +import { defineModel } from '@holo-js/db' + +// defineModel("children", {}) is only documentation and should not block Child. +export default defineModel('people', {}) +`) + + const duplicateIo = createIo(duplicateRoot) + await expect(import('../src/cli').then(module => module.runCli(['make:model', 'Person'], duplicateIo.io))).resolves.toBe(1) + expect(duplicateIo.read().stderr).toContain('Discovered duplicate model "LegacyPerson" for table "people".') + + const commentOnlyRoot = await createTempProject() + tempDirs.push(commentOnlyRoot) + await linkWorkspaceDb(commentOnlyRoot) + await ensureGeneratedSchemaPlaceholder(commentOnlyRoot, defaultProjectConfig()) + await writeProjectFile(commentOnlyRoot, 'server/models/Notes.ts', ` +// defineModel("children", {}) appears in a comment only. +/* defineModel("children", {}) also appears in a block comment only. */ +if (false) { + const tableName = 'children' + notdefineModel("children", {}) + defineModel(tableName, {}) + defineModel("parents", {}) + defineModel("child\\\\ren", {}) +} +export const holoModelPendingSchema = true +export default undefined +`) + + const commentOnlyIo = createIo(commentOnlyRoot) + await expect(import('../src/cli').then(module => module.runCli(['make:model', 'Child'], commentOnlyIo.io))).resolves.toBe(0) + await expect(readFile(join(commentOnlyRoot, 'server/models/Child.ts'), 'utf8')).resolves.toContain('export default defineModel("children", {') + }, 30000) + it('prunes all registered prunable models with no arguments and rejects explicit non-prunable models', async () => { const projectRoot = await createTempProject() tempDirs.push(projectRoot) @@ -5415,7 +5552,7 @@ export default defineAppConfig({ `) await writeProjectFile(projectRoot, 'server/models/User.mjs', ` export default { - definition: { kind: 'model', name: 'User', prunable: true }, + definition: { kind: 'model', name: 'User', table: { tableName: 'users' }, prunable: true }, async prune() { return 0 }, } `) @@ -6164,7 +6301,7 @@ export const config = { `) await writeProjectFile(projectRoot, 'app/models/User.mjs', ` export default { - definition: { kind: 'model', name: 'User', prunable: true }, + definition: { kind: 'model', name: 'User', table: { tableName: 'users' }, prunable: true }, async prune() { return 0 }, } `) @@ -6804,9 +6941,9 @@ export default { }, }, }) - expect(cliInternals.resolveConfigModuleUrl()).toContain('@holo-js/config') + expect(cliInternals.resolveConfigModuleUrl()).toContain('/config/dist/index.mjs') expect(cliInternals.resolveConfigModuleUrl(specifier => `mock:${specifier}`)).toBe('mock:@holo-js/config') - expect(cliInternals.resolveConfigModuleUrl(undefined)).toContain('/node_modules/@holo-js/config/dist/index.mjs') + expect(cliInternals.resolveConfigModuleUrl(null as never)).toContain('/node_modules/@holo-js/config/dist/index.mjs') expect(cliInternals.resolveConfigModuleUrl(() => pathToFileURL(join(workspaceRoot, 'packages/config/src/index.ts')).href)) .toBe(pathToFileURL(join(workspaceRoot, 'packages/config/dist/index.mjs')).href) expect(cliInternals.resolveConfigModuleUrl(() => pathToFileURL(join(workspaceRoot, 'packages/config/src/index.mts')).href)) @@ -6850,13 +6987,13 @@ export default { await expect(cliInternals.hasProjectDependency(projectRoot, '@holo-js/queue')).resolves.toBe(false) await writeFile(join(projectRoot, 'package.json'), JSON.stringify({ dependencies: { - '@holo-js/queue': '^0.1.2', + '@holo-js/queue': outdatedHoloPackageRange, }, }), 'utf8') await expect(cliInternals.hasProjectDependency(projectRoot, '@holo-js/queue')).resolves.toBe(true) await writeFile(join(projectRoot, 'package.json'), JSON.stringify({ devDependencies: { - '@holo-js/queue': '^0.1.2', + '@holo-js/queue': outdatedHoloPackageRange, }, }), 'utf8') await expect(cliInternals.hasProjectDependency(projectRoot, '@holo-js/queue')).resolves.toBe(true) @@ -7428,7 +7565,7 @@ export default defineMigration({ name: 'nuxt-mail-fixture', private: true, dependencies: { - nuxt: '^4.0.0', + nuxt: expectedNuxtPackageRange, }, }, null, 2)) await expect(generatorInternals.resolveProjectMailViewFramework(nuxtMailProjectRoot)).resolves.toBe('nuxt') @@ -7439,7 +7576,7 @@ export default defineMigration({ name: 'next-mail-fixture', private: true, dependencies: { - next: '^16.0.0', + next: expectedNextPackageRange, }, }, null, 2)) await expect(generatorInternals.resolveProjectMailViewFramework(nextMailProjectRoot)).resolves.toBe('next') @@ -7450,7 +7587,7 @@ export default defineMigration({ name: 'svelte-mail-fixture', private: true, dependencies: { - '@sveltejs/kit': '^2.0.0', + '@sveltejs/kit': expectedSvelteKitPackageRange, }, }, null, 2)) await expect(generatorInternals.resolveProjectMailViewFramework(svelteMailProjectRoot)).resolves.toBe('sveltekit') @@ -14267,7 +14404,7 @@ export default defineAppConfig({ `) await writeProjectFile(projectRoot, 'server/models/Session.mjs', ` export default { - definition: { kind: 'model', name: 'Session', prunable: true }, + definition: { kind: 'model', name: 'Session', table: { tableName: 'sessions' }, prunable: true }, async prune() { return 1 }, } `) @@ -14354,6 +14491,42 @@ export default { }) }) + it('falls back to the generated module when generated registry JSON is malformed', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + + await writeProjectFile(projectRoot, '.holo-js/generated/registry.json', '{') + await writeProjectFile(projectRoot, '.holo-js/generated/index.ts', ` +export const registry = { + version: 1, + generatedAt: '2026-01-01T00:00:00.000Z', + paths: { + models: 'server/models', + migrations: 'server/db/migrations', + seeders: 'server/db/seeders', + commands: 'server/commands', + jobs: 'server/jobs', + generatedSchema: '.holo-js/generated/schema.generated.ts', + }, + models: [], + migrations: [], + seeders: [], + commands: [], + jobs: [], + events: [], + listeners: [], + broadcast: [], + channels: [], + authorizationPolicies: [], + authorizationAbilities: [], +} +`) + + await withFakeBun(async () => { + await expect(loadGeneratedProjectRegistry(projectRoot)).resolves.toMatchObject({ version: 1 }) + }) + }, 30000) + it('loads generated registries from named exports and ignores invalid generated modules', async () => { const projectRoot = await createTempProject() tempDirs.push(projectRoot) @@ -14388,6 +14561,156 @@ export const registry = { }) }) + it('loads generated registries from default exports', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + + await writeProjectFile(projectRoot, '.holo-js/generated/index.ts', ` +export default { + version: 1, + generatedAt: '2026-01-01T00:00:00.000Z', + paths: { + models: 'server/models', + migrations: 'server/db/migrations', + seeders: 'server/db/seeders', + commands: 'server/commands', + jobs: 'server/jobs', + generatedSchema: '.holo-js/generated/schema.generated.ts', + }, + models: [], + migrations: [], + seeders: [], + commands: [], + jobs: [], +} +`) + + await withFakeBun(async () => { + await expect(loadGeneratedProjectRegistry(projectRoot)).resolves.toMatchObject({ version: 1 }) + }) + }, 30000) + + it('covers generated tsconfig render helpers', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + + expect(projectInternals.renderGeneratedTsconfig()).toContain('../../tsconfig.json') + await expect(renderFrameworkAwareTsconfig(projectRoot)).resolves.toContain('../../tsconfig.json') + }, 30000) + + it('renders schema runtime tables from default and named generated schema exports', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + const registry = { + version: 1, + generatedAt: '2026-01-01T00:00:00.000Z', + paths: { + models: 'server/models', + migrations: 'server/db/migrations', + seeders: 'server/db/seeders', + commands: 'server/commands', + jobs: 'server/jobs', + events: 'server/events', + listeners: 'server/listeners', + broadcast: 'server/broadcast', + channels: 'server/channels', + authorizationPolicies: 'server/policies', + authorizationAbilities: 'server/abilities', + generatedSchema: '.holo-js/generated/schema.generated.ts', + }, + models: [], + migrations: [], + seeders: [], + commands: [], + jobs: [], + events: [], + listeners: [], + broadcast: [], + channels: [], + authorizationPolicies: [], + authorizationAbilities: [], + } satisfies Parameters[1] + + await writeProjectFile(projectRoot, '.holo-js/generated/schema.generated.ts', ` +export default { + kind: 'table', + tableName: 'users', + columns: {}, + indexes: [], +} +`) + await writeGeneratedProjectRegistry(projectRoot, registry) + await expect(readFile(join(projectRoot, '.holo-js/generated/schema.mjs'), 'utf8')).resolves.toContain('users') + + await writeProjectFile(projectRoot, '.holo-js/generated/schema.generated.ts', ` +export const posts = { + kind: 'table', + tableName: 'posts', + columns: {}, + indexes: [], +} +`) + await writeGeneratedProjectRegistry(projectRoot, registry) + await expect(readFile(join(projectRoot, '.holo-js/generated/schema.mjs'), 'utf8')).resolves.toContain('posts') + }, 30000) + + it('propagates generated schema access errors other than missing files', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + const generatedSchemaPath = join(projectRoot, '.holo-js/generated/schema.generated.ts') + await writeProjectFile(projectRoot, '.holo-js/generated/schema.generated.ts', 'export const users = {}') + const accessError = Object.assign(new Error('blocked'), { code: 'EACCES' }) + vi.doMock('node:fs/promises', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + access: async (path: Parameters[0], mode?: Parameters[1]) => { + if (path === generatedSchemaPath) { + throw accessError + } + return actual.access(path, mode) + }, + } + }) + vi.resetModules() + + try { + const { writeGeneratedProjectRegistry: writeRegistryWithMockedAccess } = await import('../src/project/registry') + await expect(writeRegistryWithMockedAccess(projectRoot, { + version: 1, + generatedAt: '2026-01-01T00:00:00.000Z', + paths: { + models: 'server/models', + migrations: 'server/db/migrations', + seeders: 'server/db/seeders', + commands: 'server/commands', + jobs: 'server/jobs', + events: 'server/events', + listeners: 'server/listeners', + broadcast: 'server/broadcast', + channels: 'server/channels', + authorizationPolicies: 'server/policies', + authorizationAbilities: 'server/abilities', + generatedSchema: '.holo-js/generated/schema.generated.ts', + }, + models: [], + migrations: [], + seeders: [], + commands: [], + jobs: [], + events: [], + listeners: [], + broadcast: [], + channels: [], + authorizationPolicies: [], + authorizationAbilities: [], + })).rejects.toMatchObject({ code: 'EACCES' }) + } finally { + vi.doUnmock('node:fs/promises') + vi.resetModules() + } + }, 30000) + it('covers named exports for commands and registered runtime artifacts', async () => { const projectRoot = await createTempProject() tempDirs.push(projectRoot) @@ -14398,7 +14721,7 @@ export default defineAppConfig({}) `) await writeProjectFile(projectRoot, 'server/models/Session.mjs', ` export const SessionModel = { - definition: { kind: 'model', name: 'Session', prunable: true }, + definition: { kind: 'model', name: 'Session', table: { tableName: 'sessions' }, prunable: true }, async prune() { return 1 }, } `) @@ -14608,6 +14931,14 @@ export default { } const publishedBin = await readFile(join(built.cliPackageRoot, 'dist/bin/holo.mjs'), 'utf8') const publishedIndex = await readFile(join(built.cliPackageRoot, 'dist/index.mjs'), 'utf8') + const publishedEntrypoints = [ + publishedBin, + ...await Promise.all( + (await readdir(join(built.cliPackageRoot, 'dist'))) + .filter(fileName => fileName.endsWith('.mjs')) + .map(async fileName => readFile(join(built.cliPackageRoot, 'dist', fileName), 'utf8')), + ), + ] const executed = spawnSync('node', [built.cliBinPath, 'list'], { cwd: workspaceRoot, encoding: 'utf8', @@ -14621,6 +14952,8 @@ export default { expect(publishedBin.startsWith('#!/usr/bin/env node\n')).toBe(true) expect(publishedBin.startsWith('#!/usr/bin/env node\n#!/usr/bin/env node')).toBe(false) expect(publishedIndex.startsWith('#!/usr/bin/env node')).toBe(false) + expect(publishedEntrypoints.some(entrypoint => entrypoint.includes('workspaces:'))).toBe(false) + expect(publishedEntrypoints.some(entrypoint => entrypoint.includes('.workspaces.catalog'))).toBe(false) expect(executed.status, executed.stderr || executed.stdout).toBe(0) expect(executed.stdout).toContain('Internal Commands') }) @@ -14645,6 +14978,49 @@ export default { expect(help.stdout).toContain('Create a model and optionally related database artifacts.') }) + it('runs published make:model through the user CLI path without installing unrelated optional packages', async () => { + const projectRoot = await createTempProject() + tempDirs.push(projectRoot) + await linkWorkspaceDb(projectRoot) + + const person = runCliProcess(projectRoot, ['make:model', 'Person', '--migration']) + const child = runCliProcess(projectRoot, ['make:model', 'Child', '--migration', '--observer', '--seeder', '--factory']) + const packageJson = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8')) as { + dependencies?: Record + devDependencies?: Record + } + const dependencies = { + ...(packageJson.dependencies ?? {}), + ...(packageJson.devDependencies ?? {}), + } + + expect(person.status, person.stderr || person.stdout).toBe(0) + expect(child.status, child.stderr || child.stdout).toBe(0) + + const personModel = await readFile(join(projectRoot, 'server/models/Person.ts'), 'utf8') + const childModel = await readFile(join(projectRoot, 'server/models/Child.ts'), 'utf8') + const childObserver = await readFile(join(projectRoot, 'server/db/observers/ChildObserver.ts'), 'utf8') + const childSeeder = await readFile(join(projectRoot, 'server/db/seeders/ChildSeeder.ts'), 'utf8') + const childFactory = await readFile(join(projectRoot, 'server/db/factories/ChildFactory.ts'), 'utf8') + const migrations = await readdir(join(projectRoot, 'server/db/migrations')) + + expect(personModel).toContain('export default defineModel("people", {') + expect(childModel).toContain('export default defineModel("children", {') + expect(childModel).toContain('observers: [ChildObserver],') + expect(childObserver).toContain('export class ChildObserver') + expect(childSeeder).toContain('name: \'child\',') + expect(childFactory).toContain('defineFactory(Child, () => ({') + expect(migrations.some(fileName => fileName.endsWith('_create_people_table.ts'))).toBe(true) + expect(migrations.some(fileName => fileName.endsWith('_create_children_table.ts'))).toBe(true) + expect(dependencies['@holo-js/auth']).toBeUndefined() + expect(dependencies['@holo-js/notifications']).toBeUndefined() + expect(dependencies['@holo-js/mail']).toBeUndefined() + expect(dependencies['@holo-js/broadcast']).toBeUndefined() + expect(dependencies['@holo-js/cache']).toBeUndefined() + expect(dependencies['@holo-js/security']).toBeUndefined() + expect(dependencies['@holo-js/storage']).toBeUndefined() + }, 30_000) + it('surfaces a helpful install hint when the security package cannot be loaded', async () => { const projectRoot = await createTempProject() tempDirs.push(projectRoot) diff --git a/packages/core/tests/optional-peers.published.test.ts b/packages/core/tests/optional-peers.published.test.ts new file mode 100644 index 0000000..0e34823 --- /dev/null +++ b/packages/core/tests/optional-peers.published.test.ts @@ -0,0 +1,411 @@ +import { execFileSync } from 'node:child_process' +import { access, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { afterAll, describe, expect, it } from 'vitest' +import { + linkInstalledDependenciesForPackage, + stagePublishedPackage, +} from '../../../tests/support/published-package' + +const packageDir = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const repoRoot = resolve(packageDir, '../..') +const tempRoots: string[] = [] + +type HoloPackageName = + | 'adapter-next' + | 'adapter-nuxt' + | 'adapter-sveltekit' + | 'auth' + | 'auth-clerk' + | 'auth-social' + | 'auth-social-apple' + | 'auth-social-discord' + | 'auth-social-facebook' + | 'auth-social-github' + | 'auth-social-google' + | 'auth-social-linkedin' + | 'auth-workos' + | 'authorization' + | 'broadcast' + | 'cache' + | 'cache-db' + | 'cache-redis' + | 'config' + | 'core' + | 'db' + | 'db-mysql' + | 'db-postgres' + | 'db-sqlite' + | 'events' + | 'flux' + | 'flux-react' + | 'flux-svelte' + | 'flux-vue' + | 'forms' + | 'mail' + | 'media' + | 'notifications' + | 'queue' + | 'queue-db' + | 'queue-redis' + | 'security' + | 'session' + | 'storage' + | 'storage-s3' + | 'validation' + +type PublishedPackageCase = { + readonly packageName: HoloPackageName + readonly imports: readonly string[] +} + +type PackageManifest = { + readonly dependencies?: Record + readonly peerDependencies?: Record + readonly peerDependenciesMeta?: Record +} + +type ExecFileError = Error & { + readonly stdout?: Buffer | string + readonly stderr?: Buffer | string +} + +const cases: readonly PublishedPackageCase[] = [ + { + packageName: 'adapter-next', + imports: ['@holo-js/adapter-next', '@holo-js/adapter-next/config'], + }, + { + packageName: 'adapter-nuxt', + imports: ['@holo-js/adapter-nuxt'], + }, + { + packageName: 'adapter-sveltekit', + imports: ['@holo-js/adapter-sveltekit', '@holo-js/adapter-sveltekit/transport'], + }, + { + packageName: 'auth', + imports: ['@holo-js/auth', '@holo-js/auth/client'], + }, + { + packageName: 'auth-clerk', + imports: ['@holo-js/auth-clerk'], + }, + { + packageName: 'auth-social', + imports: ['@holo-js/auth-social'], + }, + { + packageName: 'auth-social-apple', + imports: ['@holo-js/auth-social-apple'], + }, + { + packageName: 'auth-social-discord', + imports: ['@holo-js/auth-social-discord'], + }, + { + packageName: 'auth-social-facebook', + imports: ['@holo-js/auth-social-facebook'], + }, + { + packageName: 'auth-social-github', + imports: ['@holo-js/auth-social-github'], + }, + { + packageName: 'auth-social-google', + imports: ['@holo-js/auth-social-google'], + }, + { + packageName: 'auth-social-linkedin', + imports: ['@holo-js/auth-social-linkedin'], + }, + { + packageName: 'auth-workos', + imports: ['@holo-js/auth-workos'], + }, + { + packageName: 'authorization', + imports: ['@holo-js/authorization', '@holo-js/authorization/contracts'], + }, + { + packageName: 'broadcast', + imports: ['@holo-js/broadcast', '@holo-js/broadcast/auth', '@holo-js/broadcast/contracts', '@holo-js/broadcast/runtime'], + }, + { + packageName: 'cache', + imports: ['@holo-js/cache', '@holo-js/cache/contracts'], + }, + { + packageName: 'cache-db', + imports: ['@holo-js/cache-db'], + }, + { + packageName: 'cache-redis', + imports: ['@holo-js/cache-redis'], + }, + { + packageName: 'config', + imports: ['@holo-js/config'], + }, + { + packageName: 'core', + imports: ['@holo-js/core', '@holo-js/core/runtime'], + }, + { + packageName: 'db', + imports: ['@holo-js/db'], + }, + { + packageName: 'db-mysql', + imports: ['@holo-js/db-mysql'], + }, + { + packageName: 'db-postgres', + imports: ['@holo-js/db-postgres'], + }, + { + packageName: 'db-sqlite', + imports: ['@holo-js/db-sqlite'], + }, + { + packageName: 'events', + imports: ['@holo-js/events'], + }, + { + packageName: 'flux', + imports: ['@holo-js/flux'], + }, + { + packageName: 'flux-react', + imports: ['@holo-js/flux-react'], + }, + { + packageName: 'flux-svelte', + imports: ['@holo-js/flux-svelte'], + }, + { + packageName: 'flux-vue', + imports: ['@holo-js/flux-vue'], + }, + { + packageName: 'forms', + imports: ['@holo-js/forms', '@holo-js/forms/schema', '@holo-js/forms/internal/client'], + }, + { + packageName: 'mail', + imports: ['@holo-js/mail', '@holo-js/mail/contracts'], + }, + { + packageName: 'media', + imports: ['@holo-js/media'], + }, + { + packageName: 'notifications', + imports: ['@holo-js/notifications', '@holo-js/notifications/contracts'], + }, + { + packageName: 'queue', + imports: ['@holo-js/queue'], + }, + { + packageName: 'queue-db', + imports: ['@holo-js/queue-db'], + }, + { + packageName: 'queue-redis', + imports: ['@holo-js/queue-redis'], + }, + { + packageName: 'security', + imports: ['@holo-js/security', '@holo-js/security/client', '@holo-js/security/contracts'], + }, + { + packageName: 'session', + imports: ['@holo-js/session'], + }, + { + packageName: 'storage', + imports: ['@holo-js/storage', '@holo-js/storage/runtime'], + }, + { + packageName: 'storage-s3', + imports: ['@holo-js/storage-s3'], + }, + { + packageName: 'validation', + imports: ['@holo-js/validation'], + }, +] + +const builtPackageDirs = new Map>() + +async function createTempRoot(prefix: string): Promise { + const root = await mkdtemp(join(tmpdir(), prefix)) + tempRoots.push(root) + return root +} + +async function pathExists(path: string): Promise { + try { + await access(path) + return true + } catch { + return false + } +} + +function packageSourceDir(packageName: HoloPackageName): string { + return resolve(repoRoot, 'packages', packageName) +} + +function packageSpecifier(packageName: HoloPackageName): string { + return packageName === 'core' ? '@holo-js/core' : `@holo-js/${packageName}` +} + +async function readPackageManifest(packageName: HoloPackageName): Promise { + return JSON.parse(await readFile(join(packageSourceDir(packageName), 'package.json'), 'utf8')) as PackageManifest +} + +function isHoloPackageName(value: string): value is HoloPackageName { + return value.startsWith('@holo-js/') + && cases.some(packageCase => packageSpecifier(packageCase.packageName) === value) +} + +function toHoloPackageName(value: string): HoloPackageName { + return value.slice('@holo-js/'.length) as HoloPackageName +} + +function nonOptionalPeerDependencies(manifest: PackageManifest): readonly string[] { + const optionalPeers = new Set( + Object.entries(manifest.peerDependenciesMeta ?? {}) + .filter(([, meta]) => meta.optional === true) + .map(([dependencyName]) => dependencyName), + ) + + return Object.keys(manifest.peerDependencies ?? {}) + .filter(dependencyName => !optionalPeers.has(dependencyName)) +} + +function workspaceDependencies(manifest: PackageManifest): readonly HoloPackageName[] { + return [ + ...Object.keys(manifest.dependencies ?? {}), + ...nonOptionalPeerDependencies(manifest), + ] + .filter(isHoloPackageName) + .map(toHoloPackageName) +} + +function externalPeerDependencies(manifest: PackageManifest): readonly string[] { + return nonOptionalPeerDependencies(manifest) + .filter(dependencyName => !dependencyName.startsWith('@holo-js/')) +} + +async function buildPackage(packageName: HoloPackageName): Promise { + let promise = builtPackageDirs.get(packageName) + if (!promise) { + promise = (async () => { + const buildRoot = await createTempRoot(`holo-${packageName}-published-build-`) + const outDir = join(buildRoot, 'dist') + + execFileSync('bun', ['run', '--filter', packageSpecifier(packageName), 'build'], { + cwd: repoRoot, + env: { + ...process.env, + HOLO_BUILD_OUT_DIR: outDir, + }, + stdio: 'pipe', + }) + + return await pathExists(outDir) + ? outDir + : join(packageSourceDir(packageName), 'dist') + })() + builtPackageDirs.set(packageName, promise) + } + + return await promise +} + +function formatExecOutput(value: Buffer | string | undefined): string { + if (typeof value === 'undefined') { + return '' + } + + return Buffer.isBuffer(value) ? value.toString('utf8') : value +} + +function assertPublicEntrypointsImport(appRoot: string, imports: readonly string[]): void { + const script = imports + .map(specifier => `await import(${JSON.stringify(specifier)})`) + .join('\n') + + try { + execFileSync('node', ['--input-type=module', '--eval', script], { + cwd: appRoot, + encoding: 'utf8', + stdio: 'pipe', + }) + } catch (error) { + const execError = error as ExecFileError + throw new Error([ + execError.message, + formatExecOutput(execError.stdout), + formatExecOutput(execError.stderr), + ].filter(Boolean).join('\n')) + } +} + +async function stagePackage( + appRoot: string, + packageName: HoloPackageName, + stagedPackages: Set, +): Promise { + if (stagedPackages.has(packageName)) { + return + } + stagedPackages.add(packageName) + + const sourceDir = packageSourceDir(packageName) + const packageRoot = join(appRoot, 'node_modules', '@holo-js', packageName) + const manifest = await readPackageManifest(packageName) + + for (const dependencyName of workspaceDependencies(manifest)) { + await stagePackage(appRoot, dependencyName, stagedPackages) + } + + await stagePublishedPackage(sourceDir, packageRoot, await buildPackage(packageName)) + await linkInstalledDependenciesForPackage({ + repoRoot, + nodeModulesRoot: join(appRoot, 'node_modules'), + packageJsonPath: join(sourceDir, 'package.json'), + extraDependencyNames: externalPeerDependencies(manifest), + }) +} + +async function createPublishedApp(packageCase: PublishedPackageCase): Promise { + const appRoot = await createTempRoot(`holo-${packageCase.packageName}-published-app-`) + await mkdir(join(appRoot, 'node_modules', '@holo-js'), { recursive: true }) + await writeFile(join(appRoot, 'package.json'), JSON.stringify({ + private: true, + type: 'module', + }, null, 2)) + + await stagePackage(appRoot, packageCase.packageName, new Set()) + + return appRoot +} + +afterAll(async () => { + for (const root of tempRoots.splice(0)) { + await rm(root, { recursive: true, force: true }) + } +}) + +describe('published optional peer behavior', () => { + it.each(cases)('imports $packageName public entrypoints without optional peers installed', async packageCase => { + const appRoot = await createPublishedApp(packageCase) + assertPublicEntrypointsImport(appRoot, packageCase.imports) + }, 120_000) +}) diff --git a/packages/db-mysql/tests/mysql.test.ts b/packages/db-mysql/tests/mysql.test.ts index 608344f..799d0e6 100644 --- a/packages/db-mysql/tests/mysql.test.ts +++ b/packages/db-mysql/tests/mysql.test.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { describe, expect, it, vi } from 'vitest' import { createMySQLAdapter } from '../src' @@ -34,4 +35,34 @@ describe('@holo-js/db-mysql', () => { expect(query).toHaveBeenNthCalledWith(3, 'START TRANSACTION', []) expect(query).toHaveBeenNthCalledWith(4, 'ROLLBACK', []) }) + + it('runs queries against a local MySQL server through the public adapter', async () => { + const tableName = `holo_real_usage_mysql_${randomUUID().replaceAll('-', '_')}` + const adapter = createMySQLAdapter({ + config: { + host: '127.0.0.1', + port: 3306, + user: 'root', + database: 'mysql', + }, + }) + + try { + await adapter.execute(`create table ${tableName} (id int auto_increment primary key, name varchar(255) not null)`) + const inserted = await adapter.execute( + `insert into ${tableName} (name) values (?)`, + ['real-user'], + ) + const selected = await adapter.query<{ name: string }>( + `select name from ${tableName} where id = ?`, + [inserted.lastInsertId], + ) + + expect(inserted.affectedRows).toBe(1) + expect(selected.rows).toEqual([{ name: 'real-user' }]) + } finally { + await adapter.execute(`drop table if exists ${tableName}`) + await adapter.disconnect() + } + }, 30_000) }) diff --git a/packages/db-postgres/tests/postgres.test.ts b/packages/db-postgres/tests/postgres.test.ts index cb8835a..16a824b 100644 --- a/packages/db-postgres/tests/postgres.test.ts +++ b/packages/db-postgres/tests/postgres.test.ts @@ -1,3 +1,4 @@ +import { randomUUID } from 'node:crypto' import { describe, expect, it, vi } from 'vitest' import { createPostgresAdapter } from '../src' @@ -37,4 +38,34 @@ describe('@holo-js/db-postgres', () => { expect(query).toHaveBeenNthCalledWith(3, 'BEGIN') expect(query).toHaveBeenNthCalledWith(4, 'COMMIT') }) + + it('runs queries against a local Postgres server through the public adapter', async () => { + const tableName = `holo_real_usage_postgres_${randomUUID().replaceAll('-', '_')}` + const adapter = createPostgresAdapter({ + config: { + host: '127.0.0.1', + port: 5432, + user: 'postgres', + database: 'postgres', + }, + }) + + try { + await adapter.execute(`create table ${tableName} (id serial primary key, name text not null)`) + const inserted = await adapter.execute( + `insert into ${tableName} (name) values ($1) returning id`, + ['real-user'], + ) + const selected = await adapter.query<{ name: string }>( + `select name from ${tableName} where id = $1`, + [inserted.lastInsertId], + ) + + expect(inserted.affectedRows).toBe(1) + expect(selected.rows).toEqual([{ name: 'real-user' }]) + } finally { + await adapter.execute(`drop table if exists ${tableName}`) + await adapter.disconnect() + } + }, 30_000) }) diff --git a/packages/db-sqlite/tests/sqlite.test.ts b/packages/db-sqlite/tests/sqlite.test.ts index 92a54ab..acf9d91 100644 --- a/packages/db-sqlite/tests/sqlite.test.ts +++ b/packages/db-sqlite/tests/sqlite.test.ts @@ -49,4 +49,19 @@ describe('@holo-js/db-sqlite', () => { 'COMMIT', ]) }) + + it('runs queries against a real in-memory SQLite database through the public adapter', async () => { + const adapter = createSQLiteAdapter() + + try { + await adapter.execute('create table users (id integer primary key autoincrement, name text not null)') + const inserted = await adapter.execute('insert into users (name) values (?)', ['real-user']) + const selected = await adapter.query<{ name: string }>('select name from users where id = ?', [inserted.lastInsertId]) + + expect(inserted.affectedRows).toBe(1) + expect(selected.rows).toEqual([{ name: 'real-user' }]) + } finally { + await adapter.disconnect() + } + }) }) diff --git a/packages/queue-redis/tests/real-redis.test.ts b/packages/queue-redis/tests/real-redis.test.ts new file mode 100644 index 0000000..0839526 --- /dev/null +++ b/packages/queue-redis/tests/real-redis.test.ts @@ -0,0 +1,64 @@ +import { randomUUID } from 'node:crypto' +import { describe, expect, it } from 'vitest' +import { redisQueueDriverFactory } from '../src' + +describe('@holo-js/queue-redis real Redis usage', () => { + it('dispatches, reserves, and acknowledges a job through a local Redis server', async () => { + const queueName = `holo-real-${randomUUID()}` + const driver = redisQueueDriverFactory.create({ + name: 'redis', + driver: 'redis', + connection: 'default', + queue: queueName, + retryAfter: 30, + blockFor: 1, + redis: { + host: '127.0.0.1', + port: 6379, + db: 0, + }, + }, { + async execute() { + throw new Error('The Redis queue test reserves jobs without executing them.') + }, + }) + + try { + const job = { + id: randomUUID(), + name: 'RealRedisJob', + connection: 'redis', + queue: queueName, + payload: { + ok: true, + }, + attempts: 0, + maxAttempts: 1, + createdAt: Date.now(), + } as const + + const dispatched = await driver.dispatch(job) + const reserved = await driver.reserve({ + queueNames: [queueName], + workerId: 'worker-real', + }) + + expect(dispatched.synchronous).toBe(false) + expect(dispatched.jobId).toBe(job.id) + expect(reserved?.envelope).toMatchObject({ + id: job.id, + name: job.name, + connection: job.connection, + queue: job.queue, + payload: job.payload, + }) + + if (reserved) { + await driver.acknowledge(reserved) + } + } finally { + await driver.clear({ queueNames: [queueName] }) + await driver.close() + } + }, 30_000) +}) diff --git a/packages/security/tests/real-redis.test.ts b/packages/security/tests/real-redis.test.ts new file mode 100644 index 0000000..f2c5faf --- /dev/null +++ b/packages/security/tests/real-redis.test.ts @@ -0,0 +1,47 @@ +import { randomUUID } from 'node:crypto' +import { describe, expect, it } from 'vitest' +import { createRedisRateLimitStore } from '../src' +import { createSecurityRedisAdapter } from '../src/drivers/redis-adapter' + +describe('@holo-js/security real Redis usage', () => { + it('stores rate-limit hits through the public Redis store adapter', async () => { + const prefix = `holo:security:${randomUUID()}:` + const adapter = createSecurityRedisAdapter({ + connection: 'default', + prefix, + host: '127.0.0.1', + port: 6379, + db: 0, + }) + const store = createRedisRateLimitStore(adapter) + + try { + await adapter.connect() + + const firstHit = await store.hit('login:127.0.0.1', { + maxAttempts: 2, + decaySeconds: 60, + }) + const secondHit = await store.hit('login:127.0.0.1', { + maxAttempts: 2, + decaySeconds: 60, + }) + const thirdHit = await store.hit('login:127.0.0.1', { + maxAttempts: 2, + decaySeconds: 60, + }) + const cleared = await store.clear('login:127.0.0.1') + + expect(firstHit.limited).toBe(false) + expect(firstHit.snapshot.attempts).toBe(1) + expect(secondHit.limited).toBe(false) + expect(secondHit.snapshot.attempts).toBe(2) + expect(thirdHit.limited).toBe(true) + expect(thirdHit.snapshot.attempts).toBe(3) + expect(cleared).toBe(true) + } finally { + await adapter.clearAll() + await adapter.close() + } + }, 30_000) +}) diff --git a/packages/session/tests/real-redis.test.ts b/packages/session/tests/real-redis.test.ts new file mode 100644 index 0000000..0a23678 --- /dev/null +++ b/packages/session/tests/real-redis.test.ts @@ -0,0 +1,46 @@ +import { randomUUID } from 'node:crypto' +import { describe, expect, it } from 'vitest' +import { createRedisSessionStore, type SessionRecord } from '../src' +import { createSessionRedisAdapter } from '../src/drivers/redis-adapter' + +describe('@holo-js/session real Redis usage', () => { + it('writes, reads, and deletes sessions through the public Redis store adapter', async () => { + const prefix = `holo:session:${randomUUID()}:` + const adapter = createSessionRedisAdapter({ + name: 'redis', + driver: 'redis', + connection: 'default', + host: '127.0.0.1', + port: 6379, + db: 0, + prefix, + }) + const store = createRedisSessionStore(adapter) + const now = new Date('2026-05-09T12:00:00.000Z') + const sessionId = randomUUID() + const record: SessionRecord = { + id: sessionId, + store: 'redis', + data: { + userId: 42, + }, + createdAt: now, + lastActivityAt: now, + expiresAt: new Date(Date.now() + 60_000), + } + + try { + await adapter.connect() + await store.write(record) + + const saved = await store.read(sessionId) + await store.delete(sessionId) + + expect(saved).toEqual(record) + await expect(store.read(sessionId)).resolves.toBeNull() + } finally { + await store.delete(sessionId) + await adapter.close() + } + }, 30_000) +}) diff --git a/scripts/generate-cli-workspace-catalog.mjs b/scripts/generate-cli-workspace-catalog.mjs new file mode 100644 index 0000000..696c1df --- /dev/null +++ b/scripts/generate-cli-workspace-catalog.mjs @@ -0,0 +1,31 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') + +export async function generateCliWorkspaceCatalog(root = repoRoot) { + const rootManifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) + const catalog = rootManifest.workspaces?.catalog + + if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog)) { + throw new Error('Root package.json is missing workspaces.catalog.') + } + + const workspaceCatalogPath = join(root, 'packages/cli/src/generated/workspaceCatalog.ts') + await mkdir(dirname(workspaceCatalogPath), { recursive: true }) + await writeFile( + workspaceCatalogPath, + [ + '// Generated by scripts/generate-cli-workspace-catalog.mjs. Do not edit by hand.', + '', + `export const WORKSPACE_CATALOG = Object.freeze(${JSON.stringify(catalog, null, 2)} as const)`, + '', + ].join('\n'), + 'utf8', + ) +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + await generateCliWorkspaceCatalog() +} diff --git a/scripts/generate-cli-workspace-catalog.test.mjs b/scripts/generate-cli-workspace-catalog.test.mjs new file mode 100644 index 0000000..901060b --- /dev/null +++ b/scripts/generate-cli-workspace-catalog.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict' +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { after, test } from 'node:test' +import { generateCliWorkspaceCatalog } from './generate-cli-workspace-catalog.mjs' + +let repoRoot + +after(async () => { + if (repoRoot) { + await rm(repoRoot, { recursive: true, force: true }) + } +}) + +test('workspace catalog generator creates the generated directory', async () => { + repoRoot = await mkdtemp(join(tmpdir(), 'holo-workspace-catalog-')) + await writeFile(join(repoRoot, 'package.json'), JSON.stringify({ + workspaces: { + catalog: { + eslint: '^9.0.0', + }, + }, + }, null, 2), 'utf8') + + await generateCliWorkspaceCatalog(repoRoot) + + const output = await readFile( + join(repoRoot, 'packages/cli/src/generated/workspaceCatalog.ts'), + 'utf8', + ) + assert.match(output, /"eslint"\s*:/) +}) diff --git a/scripts/validate-dependency-version-policy.mjs b/scripts/validate-dependency-version-policy.mjs new file mode 100644 index 0000000..e06c54f --- /dev/null +++ b/scripts/validate-dependency-version-policy.mjs @@ -0,0 +1,355 @@ +import { execFile } from 'node:child_process' +import { readFile } from 'node:fs/promises' +import { dirname, join, resolve } from 'node:path' +import { promisify } from 'node:util' +import { fileURLToPath, pathToFileURL } from 'node:url' + +const execFileAsync = promisify(execFile) +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const dependencySections = [ + 'dependencies', + 'devDependencies', + 'peerDependencies', + 'optionalDependencies', +] + +function isObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value) +} + +export async function readWorkspaceCatalog(root = repoRoot) { + const rootManifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) + if (!isObject(rootManifest.workspaces) || !isObject(rootManifest.workspaces.catalog)) { + return new Set() + } + + return new Set(Object.keys(rootManifest.workspaces.catalog)) +} + +export async function listTrackedAppManifests(root = repoRoot) { + const { stdout } = await execFileAsync('git', ['ls-files', 'apps/*/package.json'], { + cwd: root, + }) + + return stdout + .split('\n') + .filter(Boolean) + .map(filePath => join(root, filePath)) +} + +export async function collectAppManifestFailures(root = repoRoot) { + const catalogPackages = await readWorkspaceCatalog(root) + const manifestPaths = await listTrackedAppManifests(root) + const failures = [] + + for (const manifestPath of manifestPaths) { + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) + + for (const sectionName of dependencySections) { + const section = manifest[sectionName] + if (!isObject(section)) { + continue + } + + for (const [packageName, version] of Object.entries(section)) { + if (packageName.startsWith('@holo-js/') && version !== 'workspace:*') { + failures.push(`${manifestPath}: ${sectionName}.${packageName} must be "workspace:*" in committed apps, found "${version}".`) + continue + } + + if (!packageName.startsWith('@holo-js/') && catalogPackages.has(packageName) && version !== 'catalog:') { + failures.push(`${manifestPath}: ${sectionName}.${packageName} must use "catalog:" in committed apps, found "${version}".`) + } + } + } + } + + return failures +} + +function collectImportBindings(source) { + const bindings = new Map() + const importPattern = /import\s+([\s\S]*?)\s+from\s*['"]([^'"]+)['"]/g + for (const match of source.matchAll(importPattern)) { + const specifiers = match[1]?.trim() + const sourcePath = match[2] + if (!specifiers || !sourcePath) continue + + const addNamedBindings = (namedSpecifiers) => { + for (const rawSpecifier of namedSpecifiers.split(',')) { + const specifier = rawSpecifier.trim() + if (!specifier) continue + const aliasMatch = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(specifier) + if (!aliasMatch) continue + const importedName = aliasMatch[1] + const localName = aliasMatch[2] ?? importedName + bindings.set(localName, { importedName, sourcePath }) + } + } + + const namespaceMatch = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(specifiers) + if (namespaceMatch) { + bindings.set(namespaceMatch[1], { importedName: '*', sourcePath }) + continue + } + + if (specifiers.startsWith('{') && specifiers.endsWith('}')) { + addNamedBindings(specifiers.slice(1, -1)) + continue + } + + const defaultAndRestMatch = /^([A-Za-z_$][\w$]*)(?:\s*,\s*([\s\S]+))?$/.exec(specifiers) + if (!defaultAndRestMatch) continue + + bindings.set(defaultAndRestMatch[1], { importedName: 'default', sourcePath }) + const restSpecifiers = defaultAndRestMatch[2]?.trim() + if (!restSpecifiers) continue + + const restNamespaceMatch = /^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(restSpecifiers) + if (restNamespaceMatch) { + bindings.set(restNamespaceMatch[1], { importedName: '*', sourcePath }) + continue + } + + if (restSpecifiers.startsWith('{') && restSpecifiers.endsWith('}')) { + addNamedBindings(restSpecifiers.slice(1, -1)) + } + } + return bindings +} + +function collectNamespaceMemberNames(source, namespaceName) { + const memberNames = new Set() + const memberPattern = new RegExp(`\\b${namespaceName}\\.([A-Za-z_$][\\w$]*)\\b`, 'g') + for (const match of source.matchAll(memberPattern)) { + if (match[1]) memberNames.add(match[1]) + } + return memberNames +} + +async function collectBindingFailures({ + modulePath, + source, + localName, + binding, + forbiddenRanges, + visited, +}) { + if (!source.includes(localName)) return [] + + const resolvedModule = await readResolvedModule(modulePath, binding.sourcePath) + if (!resolvedModule) return [] + + if (binding.importedName === '*') { + const failures = [] + for (const memberName of collectNamespaceMemberNames(source, localName)) { + failures.push(...await collectImportedConstantFailures({ + modulePath: resolvedModule.path, + moduleSource: resolvedModule.source, + identifier: memberName, + forbiddenRanges, + visited, + })) + } + return failures + } + + return collectImportedConstantFailures({ + modulePath: resolvedModule.path, + moduleSource: resolvedModule.source, + identifier: binding.importedName, + forbiddenRanges, + visited, + }) +} + +function collectExportInitializer(source, exportName) { + if (exportName === 'default') { + const exportMatch = /\bexport\s+default\b/.exec(source) + if (!exportMatch) return undefined + const initializerStart = exportMatch.index + exportMatch[0].length + const nextExport = source.slice(initializerStart + 1).search(/\nexport\s+(?:const|function|class|type|interface|default)\s/) + const initializerEnd = nextExport === -1 + ? source.length + : initializerStart + 1 + nextExport + return source.slice(initializerStart, initializerEnd) + } + + const exportPattern = new RegExp(`export\\s+const\\s+${exportName}\\b`) + const exportMatch = exportPattern.exec(source) + if (!exportMatch) return undefined + + const initializerStart = source.indexOf('=', exportMatch.index) + if (initializerStart === -1) return undefined + + const nextExport = source.slice(initializerStart + 1).search(/\nexport\s+(?:const|function|class|type|interface|default)\s/) + const initializerEnd = nextExport === -1 + ? source.length + : initializerStart + 1 + nextExport + return source.slice(initializerStart + 1, initializerEnd) +} + +function collectFunctionBody(source, functionName) { + const functionPattern = new RegExp(`function\\s+${functionName}\\b[\\s\\S]*?{`) + const functionMatch = functionPattern.exec(source) + if (functionMatch) { + let depth = 1 + let index = functionMatch.index + functionMatch[0].length + while (index < source.length && depth > 0) { + const current = source[index] + if (current === '{') depth += 1 + if (current === '}') depth -= 1 + index += 1 + } + + return source.slice(functionMatch.index, index) + } + + const variableFunctionPattern = new RegExp(`(?:const|let|var)\\s+${functionName}\\b\\s*=`) + const variableFunctionMatch = variableFunctionPattern.exec(source) + if (!variableFunctionMatch) return undefined + + const initializerStart = source.indexOf('=', variableFunctionMatch.index) + if (initializerStart === -1) return undefined + + const nextDeclaration = source.slice(initializerStart + 1).search(/\n(?:export\s+)?(?:const|let|var|function|class|type|interface)\s/) + const initializerEnd = nextDeclaration === -1 + ? source.length + : initializerStart + 1 + nextDeclaration + return source.slice(variableFunctionMatch.index, initializerEnd) +} + +async function readResolvedModule(importerPath, importSource) { + if (!importSource.startsWith('.')) return undefined + const basePath = resolve(dirname(importerPath), importSource) + const candidates = [ + basePath, + `${basePath}.ts`, + `${basePath}.js`, + `${basePath}.mjs`, + join(basePath, 'index.ts'), + ] + + for (const candidate of candidates) { + try { + return { + path: candidate, + source: await readFile(candidate, 'utf8'), + } + } catch { + continue + } + } + + return undefined +} + +async function collectImportedConstantFailures({ + modulePath, + moduleSource, + identifier, + forbiddenRanges, + visited, +}) { + const visitKey = `${modulePath}:${identifier}` + if (visited.has(visitKey)) return [] + visited.add(visitKey) + + const initializer = collectExportInitializer(moduleSource, identifier) + if (!initializer) return [] + + const directForbiddenRange = forbiddenRanges.find(range => initializer.includes(range)) + if (directForbiddenRange) { + return [`${modulePath}: imported scaffold dependency constant ${identifier} must not contain ${directForbiddenRange} ranges.`] + } + + const importBindings = collectImportBindings(moduleSource) + const failures = [] + for (const [localName, binding] of importBindings) { + failures.push(...await collectBindingFailures({ + modulePath, + source: initializer, + localName, + binding, + forbiddenRanges, + visited, + })) + } + + for (const callMatch of initializer.matchAll(/\b([A-Za-z_$][\w$]*)\s*\(/g)) { + const functionBody = collectFunctionBody(moduleSource, callMatch[1]) + if (!functionBody) continue + const forbiddenRange = forbiddenRanges.find(range => functionBody.includes(range)) + if (forbiddenRange) { + failures.push(`${modulePath}: scaffold dependency helper ${callMatch[1]} used by ${identifier} must not contain ${forbiddenRange} ranges.`) + } + + for (const [localName, binding] of importBindings) { + failures.push(...await collectBindingFailures({ + modulePath, + source: functionBody, + localName, + binding, + forbiddenRanges, + visited, + })) + } + } + + return failures +} + +export async function collectScaffoldSourceFailures(root = repoRoot) { + const scaffoldPath = join(root, 'packages/cli/src/project/scaffold/framework.ts') + const source = await readFile(scaffoldPath, 'utf8') + const renderStart = source.indexOf('export function renderScaffoldPackageJson') + const renderEnd = source.indexOf('\nexport async function scaffoldProject', renderStart) + + if (renderStart === -1 || renderEnd === -1) { + return [`${scaffoldPath}: could not locate renderScaffoldPackageJson for dependency policy validation.`] + } + + const renderSource = source.slice(renderStart, renderEnd) + const forbiddenRanges = ['workspace:', 'catalog:'] + const forbiddenRange = forbiddenRanges.find(range => renderSource.includes(range)) + if (forbiddenRange) { + return [`${scaffoldPath}: generated user project manifests must not contain ${forbiddenRange} dependency ranges.`] + } + + const failures = [] + const importBindings = collectImportBindings(source) + for (const [localName, binding] of importBindings) { + failures.push(...await collectBindingFailures({ + modulePath: scaffoldPath, + source: renderSource, + localName, + binding, + forbiddenRanges, + visited: new Set(), + })) + } + + return failures +} + +export async function runDependencyVersionPolicyValidation(root = repoRoot) { + const failures = [ + ...(await collectAppManifestFailures(root)), + ...(await collectScaffoldSourceFailures(root)), + ] + + if (failures.length > 0) { + console.error('Dependency version policy failed:') + for (const failure of failures) { + console.error(`- ${failure}`) + } + return 1 + } + + console.log('Dependency version policy validated.') + return 0 +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + process.exit(await runDependencyVersionPolicyValidation()) +} diff --git a/scripts/validate-dependency-version-policy.test.mjs b/scripts/validate-dependency-version-policy.test.mjs new file mode 100644 index 0000000..a2a3296 --- /dev/null +++ b/scripts/validate-dependency-version-policy.test.mjs @@ -0,0 +1,136 @@ +import assert from 'node:assert/strict' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { afterEach, test } from 'node:test' +import { collectScaffoldSourceFailures } from './validate-dependency-version-policy.mjs' + +const tempRoots = [] + +afterEach(async () => { + await Promise.all(tempRoots.splice(0).map(repoRoot => rm(repoRoot, { + recursive: true, + force: true, + }))) +}) + +async function createTestScaffold(files) { + const repoRoot = await mkdtemp(join(tmpdir(), 'holo-dependency-policy-')) + tempRoots.push(repoRoot) + await mkdir(join(repoRoot, 'packages/cli/src/project/scaffold'), { recursive: true }) + await mkdir(join(repoRoot, 'packages/cli/src'), { recursive: true }) + + for (const [filePath, contents] of Object.entries(files)) { + const targetPath = join(repoRoot, filePath) + await mkdir(dirname(targetPath), { recursive: true }) + await writeFile(targetPath, contents.join('\n'), 'utf8') + } + + return repoRoot +} + +const frameworkSource = [ + 'import { SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS } from \'../../metadata\'', + '', + 'export function renderScaffoldPackageJson() {', + ' const devDependencies = {', + ' eslint: SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS.eslint,', + ' }', + ' return JSON.stringify({ devDependencies })', + '}', + '', + 'export async function scaffoldProject() {}', + '', +] + +test('dependency policy validator catches forbidden scaffold ranges from imported constants', async () => { + const repoRoot = await createTestScaffold({ + 'packages/cli/src/metadata.ts': [ + 'export const SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS = Object.freeze({', + ' eslint: \'catalog:\',', + '} as const)', + '', + ], + 'packages/cli/src/project/scaffold/framework.ts': frameworkSource, + }) + + const failures = await collectScaffoldSourceFailures(repoRoot) + + assert.equal(failures.length, 1) + assert.match(failures[0], /catalog:/) + assert.match(failures[0], /SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS/) +}) + +test('dependency policy validator follows default imports through arrow helpers', async () => { + const repoRoot = await createTestScaffold({ + 'packages/cli/src/metadata.ts': [ + 'import baseVersions from \'./versions\'', + '', + 'const buildVersions = () => ({', + ' eslint: baseVersions.eslint,', + '})', + '', + 'export const SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS = buildVersions()', + '', + ], + 'packages/cli/src/versions.ts': [ + 'export default Object.freeze({', + ' eslint: \'catalog:\',', + '} as const)', + '', + ], + 'packages/cli/src/project/scaffold/framework.ts': frameworkSource, + }) + + const failures = await collectScaffoldSourceFailures(repoRoot) + + assert.equal(failures.length, 1) + assert.match(failures[0], /catalog:/) + assert.match(failures[0], /default/) +}) + +test('dependency policy validator follows namespace imports through function expressions', async () => { + const repoRoot = await createTestScaffold({ + 'packages/cli/src/metadata.ts': [ + 'import * as versions from \'./versions\'', + '', + 'const buildVersions = function () {', + ' return {', + ' eslint: versions.baseVersions.eslint,', + ' }', + '}', + '', + 'export const SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS = buildVersions()', + '', + ], + 'packages/cli/src/versions.ts': [ + 'export const baseVersions = Object.freeze({', + ' eslint: \'catalog:\',', + '} as const)', + '', + ], + 'packages/cli/src/project/scaffold/framework.ts': frameworkSource, + }) + + const failures = await collectScaffoldSourceFailures(repoRoot) + + assert.equal(failures.length, 1) + assert.match(failures[0], /catalog:/) + assert.match(failures[0], /baseVersions/) +}) + +test('dependency policy validator allows valid semver ranges', async () => { + const repoRoot = await createTestScaffold({ + 'packages/cli/src/metadata.ts': [ + 'export const SCAFFOLD_BASE_DEV_DEPENDENCY_VERSIONS = Object.freeze({', + ' eslint: \'^8.0.0\',', + '} as const)', + '', + ], + 'packages/cli/src/project/scaffold/framework.ts': frameworkSource, + }) + + const failures = await collectScaffoldSourceFailures(repoRoot) + + assert.equal(failures.length, 0) +})