From 228c05d1cff2d5019159829f85bb69399fc2ced6 Mon Sep 17 00:00:00 2001 From: Carbon Date: Mon, 5 Oct 2026 18:48:52 +0800 Subject: [PATCH 1/3] fix(release): freeze 0.2 scope and verify real consumers --- .changeset/octane-native-adapter.md | 3 +- .github/workflows/integrations.yml | 5 +- docs/guide/other-integrations.md | 12 +- docs/maintainers/release-lines.md | 14 +- docs/project/compatibility.md | 16 ++- docs/project/next-release.md | 20 ++- docs/project/ssr-roadmap.md | 2 +- docs/project/status.md | 13 +- docs/reference/api.md | 2 +- docs/reference/tooling.md | 4 +- docs/zh-CN/faq.md | 2 +- docs/zh-CN/guide/bundlers.md | 2 +- docs/zh-CN/guide/other-integrations.md | 8 +- docs/zh-CN/index.md | 2 +- docs/zh-CN/project/compatibility.md | 11 +- docs/zh-CN/project/next-release.md | 16 ++- docs/zh-CN/project/release-lines.md | 8 +- docs/zh-CN/project/ssr-roadmap.md | 2 +- docs/zh-CN/project/status.md | 4 +- docs/zh-CN/reference/api.md | 5 +- docs/zh-CN/reference/tooling.md | 2 +- package.json | 9 +- packages/octane/README.md | 5 + packages/octane/package.json | 3 +- scripts/source/release-model.ts | 11 +- scripts/source/verify-packed-packages.ts | 24 +--- tests/prepare-preset-consumer.mjs | 26 ++++ tests/preset-consumer.mjs | 159 ++++++++++++++++------- tests/release-lines-contract.mjs | 28 ++++ tests/rust-hmr.mjs | 41 ++++-- tests/rust-react-source.mjs | 7 +- tests/rust-solid-source.mjs | 7 +- tests/rust-svelte-source.mjs | 7 +- tests/rust-vue-source.mjs | 7 +- tests/vite8-frameworks.mjs | 8 +- 35 files changed, 344 insertions(+), 151 deletions(-) create mode 100644 tests/prepare-preset-consumer.mjs diff --git a/.changeset/octane-native-adapter.md b/.changeset/octane-native-adapter.md index 7b3ce3cb..de4780d4 100644 --- a/.changeset/octane-native-adapter.md +++ b/.changeset/octane-native-adapter.md @@ -1,7 +1,6 @@ --- -"@vooya/octane": minor "@vooya/vite": minor "@vooya/build-core": minor --- -Add experimental native Octane 0.9 support for Rust-file components and stores through Vite 8, including generated TypeScript declarations. The adapter ships authored source for Octane's application compiler. Legacy .voo inputs, SSR and older Octane versions are outside this initial scope. +Add experimental native Octane 0.9 support for Rust-file components and stores through Vite 8, including generated TypeScript declarations. The private experimental adapter remains available only to source/tarball fixtures and is excluded from this npm release. Legacy .voo inputs, SSR and older Octane versions are outside this initial scope. diff --git a/.github/workflows/integrations.yml b/.github/workflows/integrations.yml index 92ca223f..eabeafff 100644 --- a/.github/workflows/integrations.yml +++ b/.github/workflows/integrations.yml @@ -59,6 +59,7 @@ jobs: node-version: 22 cache: npm - run: npm ci --ignore-scripts - # The fixture downloads its own pinned tools into an empty cache and asserts - # both compiler and doctor paths. Runner-provided native SDK/linker remains. + - run: npx playwright install --with-deps chromium + # The packed consumer shields system Rust, keeps the native SDK/linker, + # and verifies managed selection plus real browser dev/error recovery. - run: npm run test:preset-consumer diff --git a/docs/guide/other-integrations.md b/docs/guide/other-integrations.md index 099bc260..36242a4d 100644 --- a/docs/guide/other-integrations.md +++ b/docs/guide/other-integrations.md @@ -94,7 +94,7 @@ before starting Vite. ::: info Unreleased 0.2 evidence This setup is verified with packages built from the current checkout. It does -not establish the same five-framework compatibility for npm `@beta` packages. +not establish the same four-framework compatibility for npm `@beta` packages. Keep the ordinary Vite quickstart for the published path. See [the next-release overview](../project/next-release.md) before testing source. ::: @@ -138,12 +138,12 @@ export default defineConfig({ }); ``` -Run `npm install`, then `npm run dev` or `npm run build`. React, Solid, Svelte -and Octane retain their respective framework plugins and `vooya({ framework })` -settings. The Svelte Vite 8 fixture uses `@sveltejs/vite-plugin-svelte` 7.1.2; -Octane 0.9 requires Node 22.22.2 or newer. +Run `npm install`, then `npm run dev` or `npm run build`. React, Solid and Svelte +retain their respective framework plugins and `vooya({ framework })` settings. +The Svelte Vite 8 fixture uses `@sveltejs/vite-plugin-svelte` 7.1.2. +Octane remains a private workspace experiment outside this release matrix. -`npm run test:vite-plus-frameworks` checks all five frameworks using clean +`npm run test:vite-plus-frameworks` checks all four frameworks using clean packed consumers: `.rs` components and Stores, production browser interactions, strict generated declarations, development rebuilds, Rust error recovery and host component edits. Rust edits trigger a full page reload, not state-preserving diff --git a/docs/maintainers/release-lines.md b/docs/maintainers/release-lines.md index 7d5d72c0..6c700ae2 100644 --- a/docs/maintainers/release-lines.md +++ b/docs/maintainers/release-lines.md @@ -63,10 +63,11 @@ The following main-branch fixes stay with their 0.2 features: A Changeset marked `patch` describes a change relative to its development branch. It does not by itself establish eligibility for the 0.1 maintenance line. -## Proposed 0.2 scope freeze +## 0.2 scope freeze The theme is easier Rust toolchain setup and a more complete host integration. -These are the proposed boundaries for the first 0.2 alpha: +These are the frozen feature boundaries for the first 0.2 alpha. Fixes, tests +and documentation can continue; additional capabilities require a later batch: | Area | Included | Boundary at freeze | | --- | --- | --- | @@ -74,7 +75,12 @@ These are the proposed boundaries for the first 0.2 alpha: | Optional managed Rust preset | Pinned tools, verified downloads, isolated cache, failure recovery, doctor and system-tool selection | One optional Rust preset; host SDK/linker prerequisites remain | | SSR-safe islands | Vue lazy Store factories, React client entry, adapter hydration tests and the named Nuxt consumer | Browser-mounted WASM; no server-rendered Rust content; Next.js remains unverified | | Webpack/Rspack Rust sources | Named Vue/React components, Stores, CSS, declarations and tested watch recovery | Experimental integrations, limited to the documented fixture matrix | -| Octane and Vite+ | Native Octane adapter and the existing five-adapter Vite+ dev/build matrix | Experimental, pinned compatibility evidence; no blanket toolchain claim | +| Vite 8 and Vite+ | Packed Vue/React/Solid/Svelte dev/build, declarations and failed-build recovery | Pinned compatibility evidence; no blanket toolchain or SSR claim | + +Octane is deferred as a private workspace experiment with an independent test. +It is excluded from public build/publication candidates and the release gate. +Restoring public status requires a first-release Changeset and acceptance; +public packages may not depend on private workspace packages. Go/TinyGo experiments, a multi-language provider protocol, multilingual presets, new framework adapters, Rust server rendering, and state-preserving Rust HMR @@ -112,7 +118,7 @@ numeric target, monotonic versions, and dependency propagation against the final versions. Maintenance changesets must be patches. Existing historical release fixtures retain their original behavior without a line configuration. -Local rehearsals of the actual source graphs produce nine feature candidates +The current version plan excludes Octane and produces eight feature candidates at `0.2.0-alpha.0`, and five maintenance candidates: React/Vite beta.3 and build-core/Rspack/Webpack beta.1. Unchanged packages and historical changelog sections remain intact; repeating versioning without a new entry changes diff --git a/docs/project/compatibility.md b/docs/project/compatibility.md index 1deecc74..6a0ab532 100644 --- a/docs/project/compatibility.md +++ b/docs/project/compatibility.md @@ -37,8 +37,10 @@ optional Boolean prop (`Option::None`, rather than `Some(false)`). Solid and Svelte already preserve those values; the React/Vue fixes do not imply those adapters were absent from earlier builds. -**Unreleased source evidence:** `test:vite8-frameworks` adds separate clean tarball installs of Solid/Svelte, -Vite 8.2.1 production Chromium checks, and strict generated declaration checks. +**Unreleased source evidence:** `test:vite8-frameworks` installs Vue, React, Solid and +Svelte in separate clean tarball consumers on Vite 8.2.1. It covers production +Chromium interactions, strict generated declarations, development Rust edits, +compiler-error recovery, rapid saves and host component edits. Svelte uses `@sveltejs/vite-plugin-svelte` 7.1.2 on Vite 8. npm-registry consumer acceptance is still pending; both adapters remain experimental. SvelteKit is not covered. See [Getting started](../guide/getting-started.md) for all four configurations. @@ -56,7 +58,7 @@ published beta support table above. | React | `>=19` | Supported | Browser fixtures cover 19.0.0 and 19.2.0; React 18 is below the supported minimum | | Solid | `>=1.9 <2` | Experimental | Vite 7 production browser fixture covers a Rust-file component, callback event, and Accessor-backed Store update; adapter unit tests cover owner cleanup and late resolution | | Svelte | `>=5 <6` | Experimental | Svelte 5 + Vite 7 production Chromium fixture covers Component mount/callback, Store action, Component prop update, generated `Readable` declarations, and Component/Store owner cleanup | -| Octane | `0.9.0` | Unreleased 0.2, experimental, client only | Packed native Octane fixture with `@octanejs/vite-plugin` 0.2.1 on Vite 8 and Vite+ 0.2.9 covers Component/Store isolation, props/events, disposal/remount and strict declarations; Node >=22.22.2; SSR is not covered | +| Octane | `0.9.0` | Private workspace experiment; excluded from 0.2 | Independent packed fixture with `@octanejs/vite-plugin` 0.2.1 on Vite 8.0.16 covers Component/Store isolation, props/events, disposal/remount and strict declarations; Node >=22.22.2; no npm release or SSR claim | | React 19 Rust-file authoring | Vite 7 | Supported beta path | Production build and browser interaction cover an instance-scoped store, `useSyncExternalStore`, atomic component prop updates, and StrictMode cleanup | | Vue Vapor | Vue 3.6 experimental | Current-source fixture, experimental | Vite 8 + Vue 3.6.0-beta.17 mounts a Rust-file component when the app uses Vue's `vaporInteropPlugin`; Vapor remains an upstream Vue opt-in | @@ -71,7 +73,7 @@ Only the release evidence above establishes the npm beta boundary. | React 19 Rust-file component/store | Vite 7 production build, StrictMode mount, store action and snapshot-driven component update | `npm run test:rust-react` | | Solid 1.9 Rust-file component/store | Vite 7 production build, callback event, Store action, and signal/Accessor-driven DOM update; adapter tests cover owner-scoped disposal | `npm run test:rust-solid`; `npm run test:solid` | | Svelte 5 Rust-file component/store | Vite 7 production build, callback event, Store action, prop update, `Readable` declaration, no Chromium runtime error, and exactly one Component handle plus generated Store disposal after child unmount | `npm run test:rust-svelte` | -| Octane 0.9 Rust-file component/store | Native client components and Stores, independent instances, prop/event updates, generated types and unmount/remount | `npm run test:rust-octane`; `npm run test:vite-plus-frameworks` adds Vite+ dev recovery and host edits | +| Octane 0.9 Rust-file component/store | Native client components and Stores, independent instances, prop/event updates, generated types and unmount/remount | `npm run test:rust-octane` (independent private experiment, outside the public release gate) | | Rust-file Vite development path | Rust source edit, failed rebuild recovery, subsequent successful rebuild, and full reload | `npm run test:rust-hmr` | | Rust `rsx!` DOM runtime | Signal text/attribute updates, owned events, conditional `if`/`else`, keyed `for` reorder and DOM identity, disposal | `npm run test:rsx` | | Vue TaskList | Reactive state, keyed rows, filtering, validation error state | `npm run test:e2e` (tasks target) | @@ -94,15 +96,15 @@ SSR/Next.js and state-preserving HMR are not implied. These entries run against tarballs built from the checkout in a fresh temporary consumer. They are source evidence, not proof that installing npm `@beta` -provides Vite+ five-framework support or Webpack/Rspack Rust-file integration. +provides Vite+ four-framework support or Webpack/Rspack Rust-file integration. The evidence and boundary columns state the exact checks exercised by each toolchain; a production smoke does not imply development-server or HMR support. | Toolchain | Version range / tested version | Evidence | Boundary | | --- | --- | --- | --- | -| Vite | `>=7 <9` | `npm run test:vite8` | Vite 8.2.1 is the primary packed compatibility target; Vite 7 remains a required regression path in the repository fixtures and release gate | +| Vite | `>=7 <9` | `npm run test:vite8-frameworks`; `npm run test:vite8` | Vite 8.2.1: separate packed Vue/React/Solid/Svelte production, strict types and dev/error-recovery checks. Vite 7 remains a required regression path; the older compatibility smoke is separate. | | Vite 8 + Vue Vapor | Vite `8.2.1`; Vue `3.6.0-beta.17` | `npm run test:vite8-vapor` | Rust-file component mounts in a Vapor app with `vaporInteropPlugin`; experimental evidence only | -| Vite+ | `0.2.9`, matching core alias `0.2.9` (Vite `8.2.1`) | `npm run test:vite-plus-frameworks`; `npm run test:vite-plus` | Packed Vue/React/Solid/Svelte/Octane `.rs` production builds and Chromium interactions, strict declarations, dev rebuilds, Rust error recovery, rapid saves and host component edits. Rust edits use full reload; legacy `.voo` regression is separate. Normal npm installation uses the documented alias/overrides. | +| Vite+ | `0.2.9`, matching core alias `0.2.9` (Vite `8.2.1`) | `npm run test:vite-plus-frameworks`; `npm run test:vite-plus` | Packed Vue/React/Solid/Svelte `.rs` production builds and Chromium interactions, strict declarations, dev rebuilds, Rust error recovery, rapid saves and host component edits. Rust edits use full reload; legacy `.voo` regression is separate. Normal npm installation uses the documented alias/overrides. | | Rspack / Rsbuild | Rspack `>=2.1.10`; Rsbuild `>=2.1.13` | `npm run test:rspack` | Experimental packed Vue/React/Rslib/native-Rspack fixtures with WASM, scoped CSS, lifecycle checks, mapped diagnostics, and rebuild recovery. `test:rust-bundlers` adds packed Vue/React `.rs` production builds, Stores, props/events, scoped CSS, and unmount/remount; Vue Rust-file watch also covers failure recovery, style edits and new modules; other framework/watch combinations remain unverified. | | Webpack | `>=5` | `npm run test:webpack` | Experimental packed Vue/React production and watch fixtures with emitted WASM, scoped CSS, lifecycle checks, mapped diagnostics, and recovery. `test:rust-bundlers` adds packed Vue/React `.rs` production builds, Stores, props/events, scoped CSS, and unmount/remount; Vue Rust-file watch also covers failure recovery, style edits and new modules; other framework/watch combinations remain unverified. | diff --git a/docs/project/next-release.md b/docs/project/next-release.md index 93dbef97..84a2d392 100644 --- a/docs/project/next-release.md +++ b/docs/project/next-release.md @@ -34,12 +34,19 @@ The [preset source README](https://github.com/vooyajs/vooya/blob/main/packages/p records cache and platform details. Published beta users should continue using [system toolchain diagnostics](../reference/tooling.md#doctor). +The source acceptance command is `npm run test:preset-consumer`. It installs +seven matching tarballs into a separate Vue application, then checks ordinary +`npm run dev`, browser Component/Store interaction, Rust edits, compiler error +recovery, and `npm run build`. Ambient Rust commands are blocked while the host +SDK/linker remains available. The same test runs on Linux, macOS and Windows in +CI with a fresh managed cache; a local reused cache does not verify downloads. + ## Host integrations | Candidate | Source evidence and remaining boundary | | --- | --- | -| Octane 0.9 | Native client Component and Store adapter; Node >=22.22.2; pinned Vite 8 and Vite+ fixtures. No SSR claim. | -| Vite+ 0.2.9 | Five-framework packed dev/build fixtures with the documented aliases and overrides. Normal npm peer resolution; no claim for all Vite+ tools or versions. | +| Vite 8.2.1 | Vue/React/Solid/Svelte packed production and dev consumers, strict declarations, Rust error recovery, rapid saves and host edits. | +| Vite+ 0.2.9 | Four-framework packed dev/build fixtures with the documented aliases and overrides. Normal npm peer resolution; no claim for all Vite+ tools or versions. | | Webpack/Rspack `.rs` authoring | Vue/React components, Stores, CSS and declarations; Vue watch recovery and new-module checks. Other watch/framework combinations remain unverified. | | SSR-safe islands | Vue lazy Store creation and React client boundaries; named Nuxt production/browser fixture. Rust content still mounts in the browser; Next.js is unverified. | @@ -48,6 +55,15 @@ See [source compatibility evidence](./compatibility.md#current-source-browser-fi [SSR boundary](./ssr-roadmap.md). These are candidate evaluation paths, not instructions to combine unpublished features with npm beta packages. +## Octane is deferred + +The native Octane adapter remains a private workspace experiment, with its own +packed browser test. It is excluded from this 0.2 publication candidate and +public release gate. Do not install an unpublished `@vooya/octane` version. +Making it public later requires explicit scope review, a first-release +Changeset and registry acceptance; keeping the implementation does not commit +to a release date. + ## Rust provider extraction [PR #148](https://github.com/vooyajs/vooya/pull/148) separates the Rust build diff --git a/docs/project/ssr-roadmap.md b/docs/project/ssr-roadmap.md index 532c76bc..c79f8108 100644 --- a/docs/project/ssr-roadmap.md +++ b/docs/project/ssr-roadmap.md @@ -56,7 +56,7 @@ Do not dispatch Store actions while the snapshot is `undefined`. Shared module-l Store instances are not request-scoped. Passing Vue an already-created Store or Promise cannot defer work that has already started. -`npm run test:nuxt-ssr` builds and installs five packed Vooya packages in a fresh +`npm run test:nuxt-ssr` builds and installs six packed Vooya packages in a fresh Nuxt 4.5.2 / Vite 8.3.1 / Vue 3.5.43 consumer. It checks production Node SSR, real WASM delivery, two independent Stores, props/events, scoped CSS, client route navigation, Rust component/Store disposal, and fresh state after returning. diff --git a/docs/project/status.md b/docs/project/status.md index bc994803..cf460981 100644 --- a/docs/project/status.md +++ b/docs/project/status.md @@ -123,7 +123,7 @@ See [the release guide](../maintainers/releases.md). adapter. Both published non-Vite adapters retain transitional `.voo` fixture evidence; their `.rs` integration is unreleased 0.2 work. Webpack 4, Rollup, Turbopack, and other bundlers remain unsupported. -- The newer Vite+ five-framework dev/build matrix runs against current-source +- The newer Vite+ four-framework dev/build matrix runs against current-source tarballs. It does not expand the published beta claim or cover every Vite+ tool. - Turbopack has no compatibility claim. Its loader API evidence and unverified integration requirements are recorded in [Turbopack research](turbopack-research.md); Webpack and Rspack @@ -165,19 +165,22 @@ See [the release guide](../maintainers/releases.md). ## Unreleased 0.2 work -The optional managed preset, Octane, SSR-safe islands and Nuxt fixture, -Webpack/Rspack Rust-file integration, expanded Vite+ evidence, and proposed +The optional managed preset, SSR-safe islands and Nuxt fixture, +Webpack/Rspack Rust-file integration, expanded Vite+ evidence, and merged Rust provider extraction belong to the [next feature batch](./next-release.md). The [release-line review](../maintainers/release-lines.md) identifies two -possible 0.1 maintenance backports; neither has been published. Do not treat +merged 0.1 maintenance backports; neither has been published. Do not treat the current main branch as a maintenance release candidate. +Octane remains a private workspace experiment, excluded from this 0.2 batch +and public release gate. Its independent browser fixture is retained. + ## Next milestones Beta.0 passed strict TypeScript checks and Chromium interaction for clean Rust-file Vue and React consumers, first with packed artifacts and then with exact registry versions. Future releases retain these separate acceptance steps. -Current source adds clean Vite 8 packed consumers for Solid and Svelte. Registry +Current source adds separate clean Vite 8 dev/build consumers for all four adapters. Registry browser acceptance remains follow-up work before a broader support claim. Managed toolchain installation through `@vooya/preset` remains a separate `0.2` workstream in [#129](https://github.com/vooyajs/vooya/issues/129), not a diff --git a/docs/reference/api.md b/docs/reference/api.md index c76416cf..2d740cb9 100644 --- a/docs/reference/api.md +++ b/docs/reference/api.md @@ -1,6 +1,6 @@ # API Reference -Unless a section says otherwise, this reference describes the [published 0.1 beta packages](../project/status.md). Managed toolchain modes, Octane and SSR-safe Store factories belong to the [unreleased 0.2 work](../project/next-release.md); installing `@beta` does not enable them. +Unless a section says otherwise, this reference describes the [published 0.1 beta packages](../project/status.md). Managed toolchain modes and SSR-safe Store factories belong to the [unreleased 0.2 work](../project/next-release.md); installing `@beta` does not enable them. Octane is a separate private workspace experiment, excluded from that release batch. This page lists the public consumption paths verified from the package exports. The beta ABI may change between prereleases. Internal `@vooya/build-core` diff --git a/docs/reference/tooling.md b/docs/reference/tooling.md index 87d96fba..aeb4a926 100644 --- a/docs/reference/tooling.md +++ b/docs/reference/tooling.md @@ -17,11 +17,11 @@ may remain JavaScript when Node or a consumer tool must execute them directly. The public plugin entry is `vooya()` from `@vooya/vite`. -The plugin requires Vite `>=7`. Vite+ is tested as a separate +The plugin requires Vite `>=7 <9`. Vite+ is tested as a separate toolchain because it aliases `vite` to `@voidzero-dev/vite-plus-core`; it still uses the same `vooya()` plugin and does not create a second Vooya adapter API. The current source fixture installs normally with the documented npm aliases -and overrides; it does not require `--legacy-peer-deps`. That five-framework +and overrides; it does not require `--legacy-peer-deps`. That four-framework Vite+ evidence belongs to the unreleased 0.2 batch, not the published beta. See [Vite+ setup](../guide/other-integrations.md#vite) for the pinned configuration. diff --git a/docs/zh-CN/faq.md b/docs/zh-CN/faq.md index 2e927cc9..91e387d6 100644 --- a/docs/zh-CN/faq.md +++ b/docs/zh-CN/faq.md @@ -47,7 +47,7 @@ Vue 返回 `Ref`,React 返回 snapshot,Solid 返回 `Accessor`,Svelte 返 ## 能否接入其他 Web 框架? 架构目标是框架无关的 Web↔WASM layer。当前 adapter 证据覆盖 Vue、React,以及 -experimental Solid/Svelte 路径。main 已有未发布的 Octane adapter,以及具名 Nuxt SSR 安全岛用例; +experimental Solid/Svelte 路径。main 已有延期的 Octane 私有实验,以及具名 Nuxt SSR 安全岛用例; 它们不属于当前 npm Beta,见[发布线说明](project/release-lines.md)。其他框架仍需独立证据。 更多英文 FAQ 见[FAQ](../faq.md)。 diff --git a/docs/zh-CN/guide/bundlers.md b/docs/zh-CN/guide/bundlers.md index 611154d9..2b4177a8 100644 --- a/docs/zh-CN/guide/bundlers.md +++ b/docs/zh-CN/guide/bundlers.md @@ -7,7 +7,7 @@ experimental,不能把一次 fixture 通过理解成完整兼容承诺。 使用 `@vooya/vite` 的 `vooya()`,Vite 范围为 `>=7 <9`,Vite 8 是当前主要 兼容目标。Vue、React 和 experimental Solid/Svelte 的安装方式见[快速开始](./getting-started.md); -Solid、Svelte 的已发布 Beta 证据仅覆盖 Vite 7;main 新增的 Vite 8 五框架打包验收尚未发布。 +Solid、Svelte 的已发布 Beta 证据仅覆盖 Vite 7;main 新增的 Vite 8 四框架打包验收尚未发布。 ## Rspack / Rsbuild(experimental) diff --git a/docs/zh-CN/guide/other-integrations.md b/docs/zh-CN/guide/other-integrations.md index 44fce125..99ee0003 100644 --- a/docs/zh-CN/guide/other-integrations.md +++ b/docs/zh-CN/guide/other-integrations.md @@ -75,7 +75,7 @@ Store state 是 Svelte `Readable`,模板通过 `$state` 读取;callback even ## Vite+ ::: warning 未发布的 0.2 候选配置 -本节对应当前 main 的五框架打包验收,不能与上文的 `@beta` 安装混用。 +本节对应当前 main 的四框架打包验收,不能与上文的 `@beta` 安装混用。 复现时从同一源码提交构建并安装相关包的 tarball,运行仓库的 `npm run test:vite-plus-frameworks`;版本边界见[发布线说明](../project/release-lines.md)。 ::: @@ -110,11 +110,11 @@ export default defineConfig({ }); ``` -执行 `npm run dev` 或 `npm run build`。React、Solid、Svelte、Octane 保留各自的 +执行 `npm run dev` 或 `npm run build`。React、Solid、Svelte 保留各自的 框架插件和 `framework` 参数。Vite 8 的 Svelte fixture 使用 Svelte plugin 7.1.2; -Octane 0.9 要求 Node 22.22.2 或更新版本。 +Octane 保留为私有 workspace 实验,不纳入本轮发布矩阵。 -`test:vite-plus-frameworks` 覆盖五框架的 `.rs` 生产构建、浏览器交互、严格声明检查、 +`test:vite-plus-frameworks` 覆盖四框架的 `.rs` 生产构建、浏览器交互、严格声明检查、 开发重建、Rust 错误恢复和宿主组件更新。Rust 编辑仍使用整页刷新,不保留运行状态。 验证范围为固定版本的 dev/build,不代表所有 Vite+ 工具或 SSR 都已支持。 依赖配置依据 [Vite+ 官方迁移规则](https://viteplus.dev/guide/migrate-rules)。 diff --git a/docs/zh-CN/index.md b/docs/zh-CN/index.md index 1865aa56..a404defa 100644 --- a/docs/zh-CN/index.md +++ b/docs/zh-CN/index.md @@ -66,5 +66,5 @@ workload 仍应与真实宿主基线测量。 路径,Rspack 与 Webpack 仍是 experimental;预编译组件产品尚未正式发布。 请从[快速开始](guide/getting-started.md)开始,再看[项目状态](project/status.md)。 -main 的托管工具链、Octane、SSR/Nuxt 与 Rust-file bundler 扩展尚未发布, +main 的托管工具链、SSR/Nuxt 与 Rust-file bundler 扩展尚未发布;Octane 是延期的私有实验。 请先核对[发布线与功能批次](project/release-lines.md),不要把源码用例视为 npm Beta 能力。 diff --git a/docs/zh-CN/project/compatibility.md b/docs/zh-CN/project/compatibility.md index 98d1b08e..75842968 100644 --- a/docs/zh-CN/project/compatibility.md +++ b/docs/zh-CN/project/compatibility.md @@ -30,8 +30,9 @@ Vue、React、Solid、Svelte 的 adapter 都已随 `0.1.0-beta.0` 发布,可 Vue 将未传入的可选 Boolean prop 转成 `false` 的问题。Solid/Svelte 原有路径已 保留这些值,不需要照搬相同修复。 -当前 main 的 `test:vite8-frameworks` 已增加 Solid/Svelte 干净 tarball 安装、Vite 8.2.1 -生产构建、Chromium 交互和严格声明类型检查。Svelte 的 Vite 8 路径使用 +当前 main 的 `test:vite8-frameworks` 对 Vue/React/Solid/Svelte 分别进行干净 tarball +安装,验证 Vite 8.2.1 的生产构建、Chromium 交互、严格声明类型,以及开发时 Rust +修改、编译错误恢复、连续保存与宿主组件修改。Svelte 的 Vite 8 路径使用 `@sveltejs/vite-plugin-svelte` 7.1.2。npm registry 验收仍待补齐,支持级别仍为 实验性。这些是尚未发布的源码验收,不属于上表的已发布 Beta 证据。已发布安装路径见[快速开始](../guide/getting-started.md)。 @@ -46,9 +47,9 @@ Vue 将未传入的可选 Boolean prop 转成 `false` 的问题。Solid/Svelte | React | `>=19` | Supported | fixture 覆盖 19.0.0 与 19.2.0 | | Solid | `>=1.9 <2` | Experimental | Vite 7 browser fixture 覆盖 Rust Component、callback event 和 Accessor 驱动的 Store 更新;adapter unit test 覆盖 owner cleanup 与 late resolve | | Svelte | `>=5 <6` | Experimental | Svelte 5 + Vite 7 Chromium fixture 覆盖 Component mount/callback、Store action、prop update、`Readable` 声明,以及 Component/Store owner cleanup | -| Octane | `0.9.0` | main 未发布;Experimental,仅客户端 | 原生 plugin 0.2.1;Vite 8 与 Vite+ 0.2.9 packed fixture 覆盖 Component/Store 实例隔离、props/event、销毁重建及严格声明;Node >=22.22.2,未覆盖 SSR | -| Vite | `>=7 <9` | 主路径 | Vite 8.2.1 是主要 packed target,Vite 7 保持回归测试 | -| Vite+ | `0.2.9`,core alias `0.2.9`(Vite `8.2.1`) | main 未发布的五框架验收 | Vue/React/Solid/Svelte/Octane 的干净 tarball 安装、`.rs` 生产构建、Chromium、严格声明检查、dev 重建与错误恢复 | +| Octane | `0.9.0` | 私有 workspace 实验;不纳入 0.2 | 独立 fixture 使用 plugin 0.2.1、Vite 8.0.16,覆盖 Component/Store 实例隔离、props/event、销毁重建和声明;Node >=22.22.2;尚无 npm 发布或 SSR 承诺 | +| Vite | `>=7 <9` | 主路径 | Vite 8.2.1 四框架独立打包、类型、生产浏览器和开发恢复验收;Vite 7 保持回归测试 | +| Vite+ | `0.2.9`,core alias `0.2.9`(Vite `8.2.1`) | main 未发布的四框架验收 | Vue/React/Solid/Svelte 的干净 tarball 安装、`.rs` 生产构建、Chromium、严格声明检查、dev 重建与错误恢复 | | Rspack / Rsbuild | Rspack `>=2.1.10`;Rsbuild `>=2.1.13` | Experimental | Beta 保留旧 Vue/React/Rslib fixture;main 新增尚未发布的 Vue/React `.rs` 生产验收 | | Webpack | `>=5` | Experimental | 5.101.0、5.109.2 fixture;Webpack 4 不支持 | | Vue Vapor | Vue 3.6.0-beta.17 + Vite 8.2.1 | Experimental | 需要 Vue 的 `vaporInteropPlugin`,不是 Vooya 自己的 renderer | diff --git a/docs/zh-CN/project/next-release.md b/docs/zh-CN/project/next-release.md index efa7b924..6bf133ae 100644 --- a/docs/zh-CN/project/next-release.md +++ b/docs/zh-CN/project/next-release.md @@ -28,18 +28,30 @@ [preset 源码 README](https://github.com/vooyajs/vooya/blob/main/packages/preset/README.md)。 已发布 Beta 用户继续使用[系统工具链诊断](../reference/tooling.md)。 +源码验收命令为 `npm run test:preset-consumer`:在独立 Vue 应用中安装七个匹配的 +tarball,验证普通 `npm run dev`、浏览器 Component/Store 交互、Rust 修改、编译错误 +恢复及 `npm run build`。用例屏蔽环境中的 Rust 命令,同时保留宿主 SDK/linker。 +CI 在 Linux、macOS、Windows 上使用新的托管缓存执行同一用例;本地复用缓存通过 +不代表重新验证了下载流程。 + ## 宿主集成 | 候选能力 | 源码证据与限制 | | --- | --- | -| Octane 0.9 | 原生客户端 Component/Store adapter;Node >=22.22.2;固定版本的 Vite 8 与 Vite+ 用例;不承诺 SSR | -| Vite+ 0.2.9 | 按文档的 alias/overrides 使用正常 npm peer 解析;五框架的打包 dev/build 用例不代表全部工具或版本都兼容 | +| Vite 8.2.1 | Vue/React/Solid/Svelte 打包生产与开发用例、严格声明、Rust 错误恢复、连续保存与宿主文件修改 | +| Vite+ 0.2.9 | 按文档的 alias/overrides 使用正常 npm peer 解析;四框架的打包 dev/build 用例不代表全部工具或版本都兼容 | | Webpack/Rspack `.rs` | Vue/React Component、Store、CSS 与声明;Vue watch 恢复和新增模块检查;其他 watch/框架组合仍未验证 | | SSR 安全岛 | Vue 惰性 Store 创建与 React 客户端边界;具名 Nuxt 生产及浏览器用例;Rust 内容仍在浏览器挂载,Next.js 尚未验证 | 具体用例见[兼容性矩阵](./compatibility.md)、[实验性配置](../guide/other-integrations.md#vite) 和[SSR 范围](./ssr-roadmap.md)。这些是候选评估路径,不能与当前 npm Beta 包混用。 +## Octane 延后 + +Octane adapter 保留为私有 workspace 实验和独立打包浏览器用例,不纳入本轮 0.2 +发布候选或公共发布门禁。目前没有可安装的 `@vooya/octane` 发布版本;后续转为 +公开包需重新审查范围、提供首发 Changeset 并完成 registry 验收,不预先承诺日期。 + ## Rust provider 抽离 [PR #148](https://github.com/vooyajs/vooya/pull/148) 将 Rust 构建实现拆到 diff --git a/docs/zh-CN/project/release-lines.md b/docs/zh-CN/project/release-lines.md index 10c1b71d..c1b45b86 100644 --- a/docs/zh-CN/project/release-lines.md +++ b/docs/zh-CN/project/release-lines.md @@ -18,7 +18,7 @@ factory 同步抛错应交给 `onError`;Rust sourceRoot 的点路径规范化 ## 0.2:先冻结,再验收和发布 Alpha -本批候选围绕 Rust 工具链安装与宿主集成: +本批功能范围冻结为 Rust 工具链安装与宿主集成,后续继续修复、测试与文档: | 候选范围 | 限定边界 | | --- | --- | @@ -26,11 +26,13 @@ factory 同步抛错应交给 `onError`;Rust sourceRoot 的点路径规范化 | 可选托管 Rust preset | 固定工具、下载校验、缓存和恢复;仍需宿主 SDK/linker | | SSR 安全岛与 Nuxt | 浏览器挂载 WASM,服务器仅渲染宿主容器;Next.js 尚未验证 | | Webpack/Rspack 普通 `.rs` | Vue/React Component、Store、CSS、声明与已验证 watch 恢复;仍为实验性 | -| Octane 与 Vite+ | 原生 Octane adapter 和五框架具名 Vite+ 用例;固定版本的实验性证据 | +| Vite 8 与 Vite+ | Vue/React/Solid/Svelte 四框架打包、类型、dev/build 与错误恢复;固定版本证据,不外推 SSR | 这些能力目前只在源码中,不能由当前 `@beta` 安装获得。Go/TinyGo、新框架、 多语言 preset、Rust 服务端 HTML 和保留状态的 Rust HMR 不在本批范围。 历史实验与 RFC 可以保留,但不是本批发布要求。 +Octane 保留为私有 workspace 和独立实验用例,不纳入公共构建、发布候选和发布门禁; +后续公开需显式首发 Changeset 与验收,公开包不得依赖私有 workspace 包。 [PR #149](https://github.com/vooyajs/vooya/pull/149) 已将发布线工具和 Rust provider 抽离合入 main,并通过候选 CI。后续类型检查和测试工具整理留在 main,不扩大维护线。 @@ -46,7 +48,7 @@ Alpha 仍是预发布;Beta 与稳定版需要各自的消费反馈和发布门 release/0.1 为 `0.1.0-beta.N`。维护线只接受 patch Changeset,版本和精确依赖 由审查后的规划交给 Changesets 正式 applier 写入,不手改包版本。 -实际源码副本的本地演练得到 9 个 0.2 Alpha 候选;维护线得到 React/Vite beta.3 +当前版本规划排除 Octane 后得到 8 个 0.2 Alpha 候选;维护线得到 React/Vite beta.3 和 build-core/Rspack/Webpack beta.1 共 5 个候选。未改动的包和历史 changelog 保持不变, 重复执行版本操作不会再次升级。这些都是规划结果,尚未发布。 diff --git a/docs/zh-CN/project/ssr-roadmap.md b/docs/zh-CN/project/ssr-roadmap.md index c96d3a64..722fdb93 100644 --- a/docs/zh-CN/project/ssr-roadmap.md +++ b/docs/zh-CN/project/ssr-roadmap.md @@ -52,7 +52,7 @@ Nuxt 的真实 fixture 和 CI 已加入 main;Next.js 接入仍待完成,不 state 为 `undefined` 时不要派发 action。模块顶层共享 Store 不具备请求隔离; 传给 Vue 的现成 Store 或已启动的 Promise,也无法撤销已经开始的工作。 -新增 `npm run test:nuxt-ssr`:在独立临时项目中安装五个打包后的 Vooya 包, +新增 `npm run test:nuxt-ssr`:在独立临时项目中安装六个打包后的 Vooya 包, 使用 Nuxt 4.5.2、Vite 8.3.1、Vue 3.5.43,验证生产 Node SSR、真实 WASM、 两个独立 Store、props/事件、scoped CSS、客户端路由切换、Rust 组件与 Store 销毁以及返回页面后的全新状态。由独立 PR CI job 执行,也纳入 `verify:e2e`。 diff --git a/docs/zh-CN/project/status.md b/docs/zh-CN/project/status.md index cc3009e2..087cd4ae 100644 --- a/docs/zh-CN/project/status.md +++ b/docs/zh-CN/project/status.md @@ -50,10 +50,12 @@ source consumer 仍需要 Cargo、`wasm32-unknown-unknown` 和 ## main 的 0.2 候选(未发布) -当前源码已有可选托管 Rust preset、Octane adapter、Solid/Svelte Vite 8 与五框架 +当前源码已有可选托管 Rust preset、四框架 Vite 8 与四框架 Vite+ 打包用例、Vue/React Rust-file Webpack/Rspack 接入,以及 SSR 安全岛和 Nuxt 用例。 这些能力尚未包含在上面的 npm Beta 包中;具体证据见[兼容性矩阵](./compatibility.md)。 +Octane 保留为私有 workspace 实验,延期且不纳入本批公共发布门禁。 + 0.1 只维护已发布行为,0.2 按功能批次审查和冻结。Go/TinyGo、多语言协议、Rust 服务端 HTML 和保留状态的 Rust HMR 不在本批范围。完整范围和发布工具限制见 [发布线说明](./release-lines.md);不以当前 main 的 patch Changeset 直接生成 0.1 发布。 diff --git a/docs/zh-CN/reference/api.md b/docs/zh-CN/reference/api.md index ed8dac40..4cd4faaa 100644 --- a/docs/zh-CN/reference/api.md +++ b/docs/zh-CN/reference/api.md @@ -1,7 +1,8 @@ # API 参考 -本页参数表描述[已发布 Beta](../project/status.md) 的公开 API。main 中的 Octane、 -托管工具链和 SSR 惰性工厂等变化尚未发布,见[发布线说明](../project/release-lines.md)。 +本页参数表描述[已发布 Beta](../project/status.md) 的公开 API。main 中的 +托管工具链和 SSR 惰性工厂等变化尚未发布;Octane 保留为私有实验,不纳入本轮 0.2。 +具体见[发布线说明](../project/release-lines.md)。 这里列的是从包的公开导出和已验证消费路径中整理出的 API。prerelease ABI 可能在 预发布版本间 breaking。没有公开导出的内部 `@vooya/build-core` helper 不在这里 diff --git a/docs/zh-CN/reference/tooling.md b/docs/zh-CN/reference/tooling.md index 636b3d14..b37a065c 100644 --- a/docs/zh-CN/reference/tooling.md +++ b/docs/zh-CN/reference/tooling.md @@ -3,7 +3,7 @@ 本页的默认参数表与命令描述[已发布 Beta](../project/status.md)。main 的新增能力 单独标明“未发布”,不代表当前 npm 包已经包含。 -未发布的 mode/preset、Octane 与 SSR 配置另见[下一轮发布](../project/next-release.md)。 +未发布的 mode/preset、SSR 配置,以及延期的 Octane 私有实验,另见[下一轮发布](../project/next-release.md)。 ## `vooya()` 参数 diff --git a/package.json b/package.json index b9968f50..98f241fa 100644 --- a/package.json +++ b/package.json @@ -71,10 +71,10 @@ "test:e2e:trace": "VOOYA_E2E_TARGET=trace playwright test", "test:rsx": "npm run build:rsx-browser && VOOYA_E2E_TARGET=rsx playwright test", "verify:ci": "npm run verify:branding && npm run verify:docs && npm run test:release-contract && npm run build --workspace @vooya/vite && npm run typecheck:test-helpers && npm run verify:release-metadata && npm run format:voo:check && cargo test -p vooya-core && npm run test:preset && npm run test:build-core && npm run test:compiler && npm run test:voo && npm run test:vue && npm run test:react && npm run test:solid && npm run test:svelte && npm run typecheck && npm run typecheck:react && npm run typecheck:tasks && npm run typecheck:benchmark && npm run typecheck:scatter && npm run typecheck:trace && npm run test:quickstart && npm run test:portable && npm run pack:check", - "verify:e2e": "npm run test:compat && npm run test:rust-vue && npm run test:rust-react && npm run test:rust-solid && npm run test:rust-svelte && npm run test:rust-hmr && npm run test:astro-math && npm run test:nuxt-ssr && npm run test:rspack && npm run test:webpack && npm run test:precompiled-vue && npm run test:hmr && npm run test:e2e && npm run test:rsx && npm run test:rust-octane && npm run test:rust-bundlers && npm run test:vite8-frameworks", + "verify:e2e": "npm run test:compat && npm run test:rust-vue && npm run test:rust-react && npm run test:rust-solid && npm run test:rust-svelte && npm run test:rust-hmr && npm run test:astro-math && npm run test:nuxt-ssr && npm run test:rspack && npm run test:webpack && npm run test:precompiled-vue && npm run test:hmr && npm run test:e2e && npm run test:rsx && npm run test:rust-bundlers && npm run test:vite8-frameworks && npm run test:preset-consumer", "verify:release": "npm run verify:ci && npm run verify:e2e && npm run test:packed-release && npm run build:scripts && node scripts/generated/sync-alpha-dist-tags.js --check-published", "release:alpha": "npm run build:scripts && node scripts/generated/publish-alpha.js --channel alpha", - "build:packages": "npm run build --workspace @vooya/compiler && npm run build --workspace @vooya/build-core && npm run build --workspace @vooya/vite && npm run build --workspace @vooya/core && npm run build --workspace @vooya/vue && npm run build --workspace @vooya/react && npm run build --workspace @vooya/solid && npm run build --workspace @vooya/svelte && npm run build --workspace @vooya/rspack && npm run build --workspace @vooya/webpack && npm run build --workspace @vooya/preset && npm run build --workspace @vooya/octane", + "build:packages": "npm run build --workspace @vooya/compiler && npm run build --workspace @vooya/build-core && npm run build --workspace @vooya/vite && npm run build --workspace @vooya/core && npm run build --workspace @vooya/vue && npm run build --workspace @vooya/react && npm run build --workspace @vooya/solid && npm run build --workspace @vooya/svelte && npm run build --workspace @vooya/rspack && npm run build --workspace @vooya/webpack && npm run build --workspace @vooya/preset", "pack:check": "npm run build:packages && npm run build:scripts && node scripts/generated/verify-packed-packages.js", "verify:changelogs": "npm run build:scripts && node scripts/generated/verify-changelogs.js", "verify:changes": "npm run build:scripts && node scripts/generated/verify-change-policy.js", @@ -83,14 +83,15 @@ "test:packed-release": "npm run build:packages && node tests/packed-release-consumer.mjs", "release:beta": "npm run build:scripts && node scripts/generated/publish-alpha.js --channel beta", "test:preset": "npm test --workspace @vooya/preset", - "test:preset-consumer": "npm run build --workspace @vooya/preset && npm run build --workspace @vooya/vite && node tests/preset-consumer.mjs", + "test:preset-consumer": "npm run build --workspace @vooya/preset && npm run build --workspace @vooya/vite && node tests/prepare-preset-consumer.mjs && npm run build --workspace @vooya/vue && node tests/preset-consumer.mjs", "test:rust-octane": "node tests/rust-octane-source.mjs", "test:rust-bundlers": "npm run build:packages && node tests/rust-bundler-source.mjs", "test:vite8-frameworks": "npm run build:packages && node tests/vite8-frameworks.mjs", "test:build-core": "npm run test:provider-rust && npm test --workspace @vooya/build-core", "test:nuxt-ssr": "node tests/nuxt-ssr-consumer.mjs", "test:provider-rust": "npm test --workspace @vooya/provider-rust", - "typecheck:test-helpers": "tsc -p tests/tsconfig.helpers.json" + "typecheck:test-helpers": "tsc -p tests/tsconfig.helpers.json", + "build:experimental": "npm run build --workspace @vooya/octane" }, "devDependencies": { "@changesets/apply-release-plan": "8.1.1", diff --git a/packages/octane/README.md b/packages/octane/README.md index 6548af80..09ddd15b 100644 --- a/packages/octane/README.md +++ b/packages/octane/README.md @@ -3,6 +3,11 @@ Experimental native Octane adapter for Vooya Rust `.rs` components and stores. It uses Octane's own hooks and compiler, without a React runtime bridge. +This is a **private workspace experiment**, deferred from the first 0.2 release. +There is no published `@vooya/octane` version to install. The independent packed +fixture is retained; it is not part of the public release gate. Making this +package public requires a reviewed first-release Changeset and consumer acceptance. + The verified toolchain is **Octane 0.9.0**, **@octanejs/vite-plugin 0.2.1**, **Vite 8.0.16**, **TypeScript 5.9.3**, and **Node 22.22.2 or newer**. Older Octane versions (including 0.4.3) are not covered. diff --git a/packages/octane/package.json b/packages/octane/package.json index 5c453837..56086664 100644 --- a/packages/octane/package.json +++ b/packages/octane/package.json @@ -36,5 +36,6 @@ "octane": "0.9.0", "typescript": "5.9.3", "vite": "8.3.1" - } + }, + "private": true } diff --git a/scripts/source/release-model.ts b/scripts/source/release-model.ts index 72fe7941..4a5744c2 100644 --- a/scripts/source/release-model.ts +++ b/scripts/source/release-model.ts @@ -7,11 +7,15 @@ export function readReleaseModel(root: string) { if (config.access !== "public" || config.changelog === false || config.fixed?.length || config.linked?.length || config.ignore?.length) { throw new Error("Release configuration must generate changelogs and publish packages independently without ignored public packages."); } + const privatePackages = new Set(); const packages = readdirSync(resolve(root, "packages")).sort().flatMap((directory) => { const path = `packages/${directory}`; if (!existsSync(resolve(root, path, "package.json"))) return []; const manifest = JSON.parse(readFileSync(resolve(root, path, "package.json"), "utf8")); - if (manifest.private) return []; + if (manifest.private) { + privatePackages.add(manifest.name); + return []; + } if (!/^@vooya\/[a-z0-9-]+$/.test(manifest.name)) throw new Error(`Unexpected release package ${manifest.name}.`); return [{ directory, path, id: manifest.name as string, manifest }]; }); @@ -21,6 +25,11 @@ export function readReleaseModel(root: string) { const version = entry.manifest.version; const semver = typeof version === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/.exec(version); if (!semver || semver[4]?.split(".").some((part) => /^0\d+$/.test(part))) throw new Error(`Invalid version for ${entry.id}.`); + for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) { + for (const name of Object.keys(entry.manifest[field] ?? {})) { + if (privatePackages.has(name)) throw new Error(`${entry.id} cannot publish a ${field} reference to private workspace ${name}.`); + } + } for (const field of ["dependencies", "optionalDependencies"]) { for (const [name, range] of Object.entries(entry.manifest[field] ?? {})) { const dependency = byName.get(name); diff --git a/scripts/source/verify-packed-packages.ts b/scripts/source/verify-packed-packages.ts index 47aa1c9f..59b089b8 100644 --- a/scripts/source/verify-packed-packages.ts +++ b/scripts/source/verify-packed-packages.ts @@ -16,7 +16,6 @@ const expectedPackages = [ "@vooya/react", "@vooya/solid", "@vooya/svelte", - "@vooya/octane", "@vooya/preset", "@vooya/rspack", "@vooya/webpack", @@ -54,12 +53,6 @@ try { } for (const [subpath, definition] of Object.entries(isRecord(manifest.exports) ? manifest.exports : {})) { if (!definition || !isRecord(definition) || !("import" in definition)) continue; - if (name === "@vooya/octane" && subpath === ".") { - // Octane assigns native hook slots while compiling the consuming app. - // Shipping precompiled JS would bypass that required compiler step. - assert(definition.import === "./src/index.tsx" && definition.types === "./src/index.tsx", name, "Octane must expose its authored compiler input"); - continue; - } assert(typeof definition.import === "string" && definition.import.endsWith(".js"), name, `${subpath} must export executable JavaScript`); assert(typeof definition.types === "string" && definition.types.endsWith(".d.ts"), name, `${subpath} must export TypeScript declarations`); assert(files.has(stripPrefix(definition.import)), name, `archive is missing JavaScript for ${subpath}`); @@ -112,7 +105,6 @@ try { assert(!file.includes(".vooya/"), name, `archive contains application workspace state ${file}`); assert(!file.includes("VOOYA_COLLABORATION_LOG"), name, `archive leaks internal collaboration file ${file}`); assert(!file.includes("VOOYA_PRODUCT_OPERATING_PLAN"), name, `archive leaks internal planning file ${file}`); - if (name === "@vooya/octane" && file === "src/index.tsx") continue; assert(!file.includes("/source/") && (!file.endsWith(".ts") || file.endsWith(".d.ts")), name, `archive must contain compiled JavaScript rather than TypeScript authoring source ${file}`); } @@ -124,9 +116,9 @@ try { rmSync(packDirectory, { force: true, recursive: true }); } -function verifyTypeConsumer(packedPackages: ReadonlyMap, withOctane: boolean): void { - const consumer = join(packDirectory, withOctane ? "type-consumer-octane" : "type-consumer"); - const packages = expectedPackages.filter((name) => withOctane || name !== "@vooya/octane"); +function verifyTypeConsumer(packedPackages: ReadonlyMap, newerTypeScript: boolean): void { + const consumer = join(packDirectory, newerTypeScript ? "type-consumer-ts59" : "type-consumer"); + const packages = expectedPackages; mkdirSync(consumer, { recursive: true }); writeFileSync( join(consumer, "package.json"), @@ -141,12 +133,11 @@ function verifyTypeConsumer(packedPackages: ReadonlyMap, }), ), devDependencies: { - typescript: withOctane ? "5.9.3" : "~5.5.4", + typescript: newerTypeScript ? "5.9.3" : "~5.5.4", "@types/node": "22.12.0", - vite: withOctane ? "8.3.1" : "^7.0.0", + vite: newerTypeScript ? "8.3.1" : "^7.0.0", "solid-js": "^1.9.9", svelte: "^5.56.10", - ...(withOctane ? { octane: "0.9.0" } : {}), }, }, null, 2)}\n`, ); @@ -160,7 +151,6 @@ function verifyTypeConsumer(packedPackages: ReadonlyMap, strict: true, noEmit: true, skipLibCheck: false, - ...(withOctane ? { jsx: "preserve", jsxImportSource: "octane" } : {}), }, include: ["consumer.ts"], }, null, 2)}\n`, @@ -194,7 +184,6 @@ import { vooyaRsbuild, vooyaRspack } from "@vooya/rspack"; import { vooyaWebpack } from "@vooya/webpack"; import { defineVooyaStore as defineSolidVooyaStore } from "@vooya/solid"; import { defineVooyaStore as defineSvelteVooyaStore } from "@vooya/svelte"; -${withOctane ? 'import { defineVooyaStore as defineOctaneVooyaStore } from "@vooya/octane";' : ""} import { prepareToolchain } from "@vooya/preset"; void parseVooComponent; @@ -231,7 +220,6 @@ void vooyaRspack; void vooyaWebpack; void defineSolidVooyaStore; void defineSvelteVooyaStore; -${withOctane ? "void defineOctaneVooyaStore;" : ""} async function verifyManagedToolchainContract() { const result = await prepareToolchain(); const cargo: string = result.cargoPath; @@ -265,7 +253,7 @@ for (const name of Object.keys(provider)) assert.equal(legacy[name], provider[na if (typecheck.status !== 0) { throw new Error(`packed type consumer failed:\n${typecheck.stderr || typecheck.stdout}`); } - console.log(`Verified packed package declarations in a clean TypeScript ${withOctane ? "5.9 Octane" : "5.5"} consumer.`); + console.log(`Verified packed package declarations in a clean TypeScript ${newerTypeScript ? "5.9" : "5.5"} consumer.`); } function readManifest(name: string) { diff --git a/tests/prepare-preset-consumer.mjs b/tests/prepare-preset-consumer.mjs new file mode 100644 index 00000000..a5be50d2 --- /dev/null +++ b/tests/prepare-preset-consumer.mjs @@ -0,0 +1,26 @@ +// Build the tarball prerequisite without assuming runner-provided Rust. This +// setup cache is distinct from the consumer's independently prepared cache. +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { prepareToolchain } from "../packages/preset/lib/index.js"; +import { resolveToolchain } from "../packages/build-core/dist/index.js"; + +const root = fileURLToPath(new URL("..", import.meta.url)); +const cacheDirectory = resolve(process.env.VOOYA_PRESET_SETUP_CACHE_DIR || join(tmpdir(), "vooya-preset-test-setup")); +const prepared = await prepareToolchain({ cacheDirectory }); +// The root is a publisher workspace, not the user app. Select the explicitly +// prepared environment here; auto-discovery is tested separately in the app. +const env = { ...prepared.environment, VOOYA_TOOLCHAIN: "system" }; +const toolchain = resolveToolchain({ cwd: root, env }); +assert.equal(toolchain.cargo.path, prepared.cargoPath); +assert.ok(toolchain.rustc.path.startsWith(cacheDirectory)); +assert.ok(toolchain.wasmBindgen.path.startsWith(cacheDirectory)); +const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build", "--workspace", "@vooya/core"], { + cwd: root, env, stdio: "inherit", shell: process.platform === "win32", +}); +if (result.error) throw result.error; +assert.equal(result.status, 0, "core tarball prerequisite must build with the prepared preset toolchain"); +console.log("Built core prerequisite with the separate preset setup toolchain."); diff --git a/tests/preset-consumer.mjs b/tests/preset-consumer.mjs index 214b6522..ac78f6aa 100644 --- a/tests/preset-consumer.mjs +++ b/tests/preset-consumer.mjs @@ -1,60 +1,121 @@ -// Real opt-in smoke: build packages first. SDK/linker stays on PATH, while -// compiler selection is forced to the isolated managed installation. +// Install only tarballs into an independent app. Host SDK/linker remains available; +// PATH shims reject ambient Rust tools while the preset uses its absolute paths. import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs"; +import { createServer } from "node:net"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, copyFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { delimiter, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { prepareToolchain } from "../packages/preset/lib/index.js"; -import { buildApplication, resolveToolchain } from "../packages/build-core/dist/index.js"; -import { inspectToolchain } from "../packages/vite/dist/doctor.js"; +import { chromium } from "playwright"; +import { startDevServer, stopDevServer } from "./helpers/dev-server.mjs"; const root = mkdtempSync(join(tmpdir(), "vooya-preset-consumer-")); const repositoryRoot = fileURLToPath(new URL("..", import.meta.url)); -const cacheDirectory = process.env.VOOYA_CACHE_DIR || join(root, "cache"); +const cacheDirectory = resolve(process.env.VOOYA_CACHE_DIR || join(root, "cache")); const npm = process.platform === "win32" ? "npm.cmd" : "npm"; -function run(args, cwd) { - const result = spawnSync(npm, args, { cwd, encoding: "utf8", shell: process.platform === "win32" }); - assert.equal(result.status, 0, result.stderr || result.error?.message); +let server; +let browser; +let output = ""; +let spawnError; +function run(args, cwd, env = process.env) { + const result = spawnSync(npm, args, { cwd, env, encoding: "utf8", shell: process.platform === "win32" }); + assert.equal(result.status, 0, result.stderr || result.stdout || result.error?.message); return result.stdout; } try { - const applicationRoot = join(root, "app"); - mkdirSync(join(applicationRoot, "src"), { recursive: true }); - writeFileSync(join(applicationRoot, "package.json"), JSON.stringify({ name: "preset-consumer", private: true })); - const packed = JSON.parse(run(["pack", "--workspace", "@vooya/preset", "--pack-destination", root, "--json"], repositoryRoot)); - run(["install", "--save-dev", "--ignore-scripts", "--no-audit", "--no-fund", join(root, packed[0].filename)], applicationRoot); - - const [first, second] = await Promise.all([prepareToolchain({ cacheDirectory }), prepareToolchain({ cacheDirectory })]); - assert.equal(first.cargoPath, second.cargoPath); - assert.ok(first.cargoPath.startsWith(resolve(cacheDirectory))); - const env = { ...process.env, VOOYA_CACHE_DIR: cacheDirectory, VOOYA_TOOLCHAIN: "auto", RUSTC: "/deliberately-missing-system-rustc" }; - const toolchain = resolveToolchain({ cwd: applicationRoot, env }); - assert.equal(toolchain.cargoSelection, "managed"); - assert.equal(toolchain.cargo.path, first.cargoPath); - assert.match(toolchain.rustc.version, /1\.94\.0/); - const explicit = resolveToolchain({ cwd: applicationRoot, env: toolchain.environment, mode: "system", cargoPath: toolchain.cargo.path }); - assert.equal(explicit.cargoSelection, "explicit"); - const report = inspectToolchain({ cwd: applicationRoot, env }); - assert.equal(report.ok, true, JSON.stringify(report.results)); - assert.equal(report.toolchain.cargo.path, toolchain.cargo.path); - const source = readFileSync(new URL("./fixtures/rust-vue/src/Store.rs", import.meta.url)); - const sourcePath = join(applicationRoot, "src/Store.rs"); - writeFileSync(sourcePath, source); - // Leave toolchain unspecified: ordinary build-core calls discover the packed preset. - const previousCache = process.env.VOOYA_CACHE_DIR; - process.env.VOOYA_CACHE_DIR = cacheDirectory; - try { - const result = buildApplication({ applicationRoot, rust: { sourceRoot: "src" }, buildMode: "development" }); - assert.ok(result.wasm.bytes.length > 0); - writeFileSync(sourcePath, "this is not Rust;\n"); - assert.throws(() => buildApplication({ applicationRoot, rust: { sourceRoot: "src" }, buildMode: "development" }), /Cargo build failed/); - writeFileSync(sourcePath, source); - const recovered = buildApplication({ applicationRoot, rust: { sourceRoot: "src" }, buildMode: "production" }); - assert.ok(recovered.wasm.bytes.length > 0); - } finally { - if (previousCache === undefined) delete process.env.VOOYA_CACHE_DIR; - else process.env.VOOYA_CACHE_DIR = previousCache; + const app = join(root, "app"); + mkdirSync(join(app, "src"), { recursive: true }); + const packs = join(root, "packs"); + mkdirSync(packs); + const tarballs = ["preset", "core", "compiler", "provider-rust", "build-core", "vite", "vue"].map(name => { + const [packed] = JSON.parse(run(["pack", "--workspace", `@vooya/${name}`, "--pack-destination", packs, "--json"], repositoryRoot)); + return join(packs, packed.filename); + }); + writeFileSync(join(app, "package.json"), JSON.stringify({ name: "preset-consumer", private: true, type: "module", scripts: { dev: "vite", build: "vite build", doctor: "vooya doctor --json" }, dependencies: { vue: "^3.5.0" }, devDependencies: { vite: "^7.0.0", "@vitejs/plugin-vue": "^6.0.0" } })); + run(["install", "--save-dev", "--no-audit", "--no-fund", ...tarballs], app); + for (const name of ["Counter.rs", "Counter.css", "Store.rs", "main.js"]) copyFileSync(join(repositoryRoot, "tests/fixtures/rust-vue/src", name), join(app, "src", name)); + copyFileSync(join(repositoryRoot, "tests/fixtures/rust-vue/index.html"), join(app, "index.html")); + writeFileSync(join(app, "vite.config.js"), 'import {defineConfig} from "vite"; import vue from "@vitejs/plugin-vue"; import {vooya} from "@vooya/vite"; export default defineConfig({plugins:[vue(),vooya({framework:"vue"})]});'); + writeFileSync(join(app, "src/App.vue"), ``); + const blocked = join(root, "blocked-tools"); + mkdirSync(blocked); + for (const tool of ["cargo", "rustc", "rustup", "wasm-bindgen"]) { + writeFileSync(join(blocked, tool + (process.platform === "win32" ? ".cmd" : "")), process.platform === "win32" ? '@echo off\necho AMBIENT_RUST_BLOCKED 1>&2\nexit /b 97\n' : '#!/bin/sh\necho AMBIENT_RUST_BLOCKED >&2\nexit 97\n', { mode: 0o755 }); } - console.log(`Managed Rust/WASM passed (${process.platform}/${process.arch}): packed preset, auto discovery, doctor, concurrent cache reuse, dev/prod and recovery after a Rust error.`); -} finally { rmSync(root, { recursive: true, force: true }); } + const env = { ...process.env, PATH: [blocked, dirname(process.execPath), process.env.PATH ?? process.env.Path ?? ""].join(delimiter), VOOYA_CACHE_DIR: cacheDirectory, VOOYA_TOOLCHAIN: "auto", RUSTC: join(blocked, "rustc") }; + if (process.platform === "win32") delete env.Path; + for (const tool of ["cargo", "rustc", "rustup", "wasm-bindgen"]) { + const ambient = spawnSync(tool, ["--version"], { env, encoding: "utf8", shell: process.platform === "win32" }); + assert.equal(ambient.status, 97, `ambient ${tool} must be blocked`); + } + writeFileSync(join(app, "probe.mjs"), `import assert from 'node:assert/strict'; import {prepareToolchain} from '@vooya/preset'; import {resolveToolchain} from '@vooya/build-core'; const [a,b]=await Promise.all([prepareToolchain(),prepareToolchain()]); assert.equal(a.cargoPath,b.cargoPath); const t=resolveToolchain({cwd:process.cwd(),env:process.env}); assert.equal(t.cargoSelection,'managed'); assert.equal(t.cargo.path,a.cargoPath); assert.ok(t.cargo.path.startsWith(process.env.VOOYA_CACHE_DIR)); assert.match(t.rustc.version,/1\\.94\\.0/); const explicit=resolveToolchain({cwd:process.cwd(),env:t.environment,mode:'system',cargoPath:t.cargo.path}); assert.equal(explicit.cargoSelection,'explicit'); console.log(JSON.stringify(t));`); + const probe = spawnSync(process.execPath, ["probe.mjs"], { cwd: app, env, encoding: "utf8" }); + assert.equal(probe.status, 0, probe.stderr); + const toolchain = JSON.parse(probe.stdout); + const doctor = JSON.parse(run(["run", "--silent", "doctor"], app, env)); + assert.equal(doctor.ok, true, JSON.stringify(doctor)); + assert.equal(doctor.schemaVersion, 1); + assert.equal(doctor.cargo.selection, "managed"); + assert.equal(doctor.cargo.path, toolchain.cargo.path); + assert.ok(doctor.rustc.path.startsWith(cacheDirectory)); + const port = await availablePort(); + server = startDevServer(npm, ["run", "dev", "--", "--host", "127.0.0.1", "--port", String(port), "--strictPort"], { cwd: app, env, shell: process.platform === "win32", stdio: ["ignore", "pipe", "pipe"] }); + server.on("error", error => { spawnError = error; }); + server.stdout.on("data", data => { output += data; }); + server.stderr.on("data", data => { output += data; }); + const url = `http://127.0.0.1:${port}`; + await waitUntil(async () => { if (spawnError) throw spawnError; assert.equal(server.exitCode, null, output); assert.equal(server.signalCode, null, output); try { return (await fetch(url, { signal: AbortSignal.timeout(1000) })).ok; } catch { return false; } }, "dev server startup"); + browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + page.setDefaultTimeout(120_000); + await page.goto(url); + await page.getByRole("button", { name: "Count: 0", exact: true }).waitFor(); + await page.getByRole("button", { name: "Store: 0", exact: true }).click(); + await page.getByRole("button", { name: "Store: 1", exact: true }).waitFor(); + await page.getByRole("button", { name: "Count: 1", exact: true }).waitFor(); + const path = join(app, "src/Counter.rs"); + const source = readFileSync(path, "utf8"); + writeFileSync(path, source.replace('Count: {}', 'Managed edit: {}')); + await page.getByRole("button", { name: /^Managed edit:/ }).waitFor(); + const errorStart = output.length; + writeFileSync(path, source + '\nfn deliberately_invalid( {\n'); + await waitUntil(() => /error|failed/i.test(output.slice(errorStart)), "Rust error diagnostic"); + await page.locator("vite-error-overlay").waitFor({ state: "attached" }); + writeFileSync(path, source.replace('Count: {}', 'Recovered: {}')); + await page.getByRole("button", { name: /^Recovered:/ }).waitFor(); + await page.locator("vite-error-overlay").waitFor({ state: "detached" }); + // Exercise the same ordinary build command while ambient Rust stays blocked. + await browser.close(); browser = undefined; + await stopServer(); + run(["run", "build"], app, env); + console.log(`Managed packed consumer passed (${process.platform}/${process.arch}): ordinary dev/browser Component+Store, Rust edit, compiler error/recovery, production build, concurrent cache and doctor; ambient Rust commands blocked, host SDK retained.`); +} catch (error) { + if (output) console.error(output); + throw error; +} finally { + try { await browser?.close(); } finally { + try { await stopServer(); } finally { + rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 }); + } + } +} +async function waitUntil(check, label) { + const deadline = Date.now() + 180_000; + while (Date.now() < deadline) { + if (await check()) return; + await new Promise(resolve => setTimeout(resolve, 250)); + } + throw new Error(`Timed out: ${label}\n${output}`); +} +async function stopServer() { + if (!server) return; + const child = server; server = undefined; + await stopDevServer(child); +} +async function availablePort() { + const socket = createServer(); + await new Promise((resolve, reject) => { socket.once("error", reject); socket.listen(0, "127.0.0.1", resolve); }); + const { port } = socket.address(); + await new Promise(resolve => socket.close(resolve)); + return port; +} diff --git a/tests/release-lines-contract.mjs b/tests/release-lines-contract.mjs index 284975b0..4a89dea0 100644 --- a/tests/release-lines-contract.mjs +++ b/tests/release-lines-contract.mjs @@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process"; import { createRequire } from "node:module"; const { satisfies } = createRequire(import.meta.resolve("@changesets/assemble-release-plan"))("semver"); import { readReviewedReleasePlan } from "../scripts/generated/release-plan.js"; +import { readReleaseModel, readChangesets } from "../scripts/generated/release-model.js"; import { readReleaseChannel, readReleaseLine, validateReleaseVersion } from "../scripts/generated/release-channel.js"; const root = fileURLToPath(new URL("..", import.meta.url)); @@ -80,6 +81,33 @@ try { assert.equal(read(directory, "packages/vue/package.json").version, "0.2.0-alpha.1"); assert.equal(read(directory, "packages/core/package.json").version, "0.1.0-beta.0"); + const experimental = fixture("private-experiment", feature, [ + ["core", "0.1.0-beta.0"], ["octane", "0.0.0"], + ]); + const octane = read(experimental, "packages/octane/package.json"); + write(experimental, "packages/octane/package.json", { ...octane, private: true }); + changeset(experimental, "core-fix", [["core", "patch"]], "Public releases exclude private experiments."); + assert.deepEqual(readReleaseModel(experimental).packages.map(({ id }) => id), ["@vooya/core"]); + assert.deepEqual((await readReviewedReleasePlan(experimental)).plan.releases.map(({ name }) => name), ["@vooya/core"]); + version(experimental); + assert.equal(read(experimental, "packages/octane/package.json").version, "0.0.0"); + assert.deepEqual(read(experimental, ".changeset/release.json").packages.map(({ name }) => name), ["@vooya/core"]); + changeset(experimental, "private-note", [["octane", "minor"]], "Private packages cannot enter a release changeset."); + assert.throws(() => readChangesets(experimental), /unknown Vooya package @vooya\/octane/); + rmSync(resolve(experimental, ".changeset/private-note.md")); + const core = read(experimental, "packages/core/package.json"); + for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) { + write(experimental, "packages/core/package.json", { ...core, [field]: { "@vooya/octane": "0.0.0" } }); + assert.throws(() => readReleaseModel(experimental), /cannot publish.*private workspace/); + } + write(experimental, "packages/core/package.json", core); + // Making an experimental package public later must not implicitly approve its first release. + write(experimental, "packages/octane/package.json", octane); + changeset(experimental, "next-fix", [["core", "patch"]], "A separate change does not approve an experimental package."); + await assert.rejects(readReviewedReleasePlan(experimental), /explicit first-release changeset/); + changeset(experimental, "approve-octane", [["octane", "minor"]], "Explicitly approve the first public release after separate acceptance."); + assert.equal((await readReviewedReleasePlan(experimental)).plan.releases.find(({ name }) => name === "@vooya/octane").newVersion, "0.2.0-alpha.0"); + const maintenance = fixture("maintenance", { baseVersion: "0.1.0", channel: "beta", branch: "release/0.1" }, [ ["core", "0.1.0-beta.0"], ["compiler", "0.1.0-beta.0"], ["build-core", "0.1.0-beta.0", { "@vooya/core": "0.1.0-beta.0", "@vooya/compiler": "0.1.0-beta.0" }], diff --git a/tests/rust-hmr.mjs b/tests/rust-hmr.mjs index 1b836f4e..6fdaa16a 100644 --- a/tests/rust-hmr.mjs +++ b/tests/rust-hmr.mjs @@ -16,6 +16,7 @@ const framework = process.argv[2] ?? "vue"; let output = ""; let server; let browser; +const originalSources = new Map(); try { if (!process.env.VOOYA_RUST_FIXTURE_ROOT) { @@ -39,6 +40,7 @@ try { const componentPath = resolve(project, "src/Counter.rs"); const source = readFileSync(componentPath, "utf8"); + originalSources.set(componentPath, source); writeFileSync(componentPath, source.replace("Count: {}", "HMR: {}")); await page.getByRole("button", { name: "HMR: 0" }).first().waitFor(); @@ -58,19 +60,36 @@ try { if (framework === "octane") await page.getByRole("button", { name: "Store A: 0", exact: true }).click(); else await page.locator(".store-add").click(); await page.getByRole("button", { name: "Rapid 4: 1" }).first().waitFor(); - if (process.env.VOOYA_VITE_PLUS) { - const extension = { vue: "vue", react: "jsx", solid: "jsx", svelte: "svelte", octane: "tsx" }[framework]; - const hostPath = resolve(project, `src/App.${extension}`); - const host = readFileSync(hostPath, "utf8"); - writeFileSync(hostPath, host.replace("Selected ", "Host updated ")); - await page.getByText(/^Host updated /).first().waitFor(); - } + const extension = { vue: "vue", react: "jsx", solid: "jsx", svelte: "svelte", octane: "tsx" }[framework]; + const hostPath = resolve(project, `src/App.${extension}`); + const host = readFileSync(hostPath, "utf8"); + originalSources.set(hostPath, host); + writeFileSync(hostPath, host.replace("Selected ", "Host updated ")); + await page.getByText(/^Host updated /).first().waitFor(); if (unexpectedErrors.length) throw new Error(`Unexpected browser errors: ${unexpectedErrors.join("\n")}`); - console.log(`Verified ${framework} Rust-file HMR rebuild, failure recovery, rapid-save coalescing, and full reload.`); + console.log(`Verified ${framework} Rust-file HMR rebuild, failure recovery, rapid-save coalescing, host edits, and full reload.`); } finally { - await browser?.close(); - await stopDevServer(server); - rmSync(temporaryRoot, { force: true, recursive: true }); + try { + await browser?.close(); + } finally { + try { + await stopDevServer(server); + // stopDevServer also kills Vite+ descendants. Wait for the child's pipes + // to close before restoring source or deleting its working directory. + if (server && server.exitCode === null && server.signalCode === null) { + await new Promise((resolveClose, rejectClose) => { + const timer = setTimeout(() => rejectClose(new Error("Development server did not exit after cleanup.")), 5000); + server.once("close", () => { clearTimeout(timer); resolveClose(); }); + }); + } + } finally { + try { + for (const [path, source] of originalSources) writeFileSync(path, source); + } finally { + rmSync(temporaryRoot, { force: true, recursive: true }); + } + } + } } function collectOutput(chunk) { diff --git a/tests/rust-react-source.mjs b/tests/rust-react-source.mjs index 094d57d3..334a28fd 100644 --- a/tests/rust-react-source.mjs +++ b/tests/rust-react-source.mjs @@ -63,8 +63,11 @@ async function verifyBrowser() { } if (errors.length > 0) throw new Error(`Rust-file React fixture had browser errors:\n${errors.join("\n")}`); } finally { - await browser.close(); - await new Promise((resolveClose) => server.close(resolveClose)); + try { + await browser.close(); + } finally { + await new Promise((resolveClose) => server.close(resolveClose)); + } } } diff --git a/tests/rust-solid-source.mjs b/tests/rust-solid-source.mjs index 1445ff6e..dcdd1179 100644 --- a/tests/rust-solid-source.mjs +++ b/tests/rust-solid-source.mjs @@ -67,8 +67,11 @@ async function verifyBrowser() { await page.getByText("Selected 1").waitFor(); if (errors.length > 0) throw new Error(`Rust-file Solid fixture had browser errors:\n${errors.join("\n")}`); } finally { - await browser.close(); - await new Promise((resolveClose) => server.close(resolveClose)); + try { + await browser.close(); + } finally { + await new Promise((resolveClose) => server.close(resolveClose)); + } } } diff --git a/tests/rust-svelte-source.mjs b/tests/rust-svelte-source.mjs index 43661f1d..2ef93e95 100644 --- a/tests/rust-svelte-source.mjs +++ b/tests/rust-svelte-source.mjs @@ -73,8 +73,11 @@ async function verifyBrowser() { )); if (errors.length > 0) throw new Error(`Rust-file Svelte fixture had browser errors:\n${errors.join("\n")}`); } finally { - await browser.close(); - await new Promise((resolveClose) => server.close(resolveClose)); + try { + await browser.close(); + } finally { + await new Promise((resolveClose) => server.close(resolveClose)); + } } } diff --git a/tests/rust-vue-source.mjs b/tests/rust-vue-source.mjs index 9c73ae2e..cd15d29d 100644 --- a/tests/rust-vue-source.mjs +++ b/tests/rust-vue-source.mjs @@ -77,8 +77,11 @@ async function verifyBrowser() { } if (errors.length > 0) throw new Error(`Rust-file Vue fixture had browser errors:\n${errors.join("\n")}`); } finally { - await browser.close(); - await new Promise((resolveClose) => server.close(resolveClose)); + try { + await browser.close(); + } finally { + await new Promise((resolveClose) => server.close(resolveClose)); + } } } diff --git a/tests/vite8-frameworks.mjs b/tests/vite8-frameworks.mjs index e49cfab3..0223b361 100644 --- a/tests/vite8-frameworks.mjs +++ b/tests/vite8-frameworks.mjs @@ -10,7 +10,7 @@ const target = process.argv[2] ?? "vite8"; if (!["vite8", "vite-plus"].includes(target)) throw new Error(`Unknown target: ${target}`); const plus = target === "vite-plus"; const label = plus ? "Vite+ 0.2.9" : "Vite 8.2.1"; -const frameworks = plus ? ["vue", "react", "solid", "svelte", "octane"] : ["solid", "svelte"]; +const frameworks = ["vue", "react", "solid", "svelte"]; const temporary = mkdtempSync(resolve(tmpdir(), `vooya-${target}-frameworks-`)); try { mkdirSync(resolve(temporary, "packages")); @@ -41,8 +41,8 @@ try { const environment = { VOOYA_RUST_FIXTURE_ROOT: project, ...(plus ? { VOOYA_VITE_PLUS: "1" } : {}) }; run(process.execPath, [resolve(root, `tests/rust-${framework}-source.mjs`)], root, environment); verifyDeclarations(project, framework); - if (plus) run(process.execPath, [resolve(root, "tests/rust-hmr.mjs"), framework], root, environment); - console.log(`Verified packed ${label} ${framework}: production browser, strict declarations${plus ? ", dev reload and Rust error recovery" : ""}.`); + run(process.execPath, [resolve(root, "tests/rust-hmr.mjs"), framework], root, environment); + console.log(`Verified packed ${label} ${framework}: production browser, strict declarations, dev reload, Rust error recovery, rapid saves and host edits.`); } } finally { if (!process.env.VOOYA_KEEP_VITE8_FRAMEWORKS) rmSync(temporary, { recursive: true, force: true }); @@ -55,13 +55,11 @@ function verifyDeclarations(project, framework) { react: 'const count: number | undefined = state?.count;', solid: 'const count: number | undefined = state()?.count;', svelte: 'state.subscribe((snapshot) => { const count: number | undefined = snapshot?.count; });', - octane: 'const count: number | undefined = state?.count;', }; writeFileSync(resolve(project, "tsconfig.contract.json"), JSON.stringify({ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "Bundler", strict: true, noEmit: true, skipLibCheck: false, allowArbitraryExtensions: true, rootDirs: [".", ".vooya/types"], types: [], - ...(framework === "octane" ? { jsx: "preserve", jsxImportSource: "octane" } : {}), }, include: ["src/type-probe.ts"] }, null, 2)); writeFileSync(resolve(project, "src/type-probe.ts"), `import { useCart } from "./Store.rs"; import Counter from "./Counter.rs"; From 77c0e130909f1899694128593cbed62471c274f3 Mon Sep 17 00:00:00 2001 From: Carbon Date: Mon, 5 Oct 2026 19:01:07 +0800 Subject: [PATCH 2/3] fix(test): compare canonical managed toolchain paths --- docs/maintainers/release-lines.md | 6 ++++-- docs/zh-CN/project/release-lines.md | 4 +++- tests/prepare-preset-consumer.mjs | 11 +++++++---- tests/preset-consumer.mjs | 28 +++++++++++++++++++++------- 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/maintainers/release-lines.md b/docs/maintainers/release-lines.md index 6c700ae2..8efab7a5 100644 --- a/docs/maintainers/release-lines.md +++ b/docs/maintainers/release-lines.md @@ -79,8 +79,10 @@ and documentation can continue; additional capabilities require a later batch: Octane is deferred as a private workspace experiment with an independent test. It is excluded from public build/publication candidates and the release gate. -Restoring public status requires a first-release Changeset and acceptance; -public packages may not depend on private workspace packages. +Restoring public status requires a first-release Changeset and acceptance. +Public packages may not reference private workspaces in `dependencies`, +`optionalDependencies` or `peerDependencies`. Development-only tooling does not +become an installed consumer dependency through `devDependencies`. Go/TinyGo experiments, a multi-language provider protocol, multilingual presets, new framework adapters, Rust server rendering, and state-preserving Rust HMR diff --git a/docs/zh-CN/project/release-lines.md b/docs/zh-CN/project/release-lines.md index c1b45b86..c1b9bc18 100644 --- a/docs/zh-CN/project/release-lines.md +++ b/docs/zh-CN/project/release-lines.md @@ -32,7 +32,9 @@ factory 同步抛错应交给 `onError`;Rust sourceRoot 的点路径规范化 多语言 preset、Rust 服务端 HTML 和保留状态的 Rust HMR 不在本批范围。 历史实验与 RFC 可以保留,但不是本批发布要求。 Octane 保留为私有 workspace 和独立实验用例,不纳入公共构建、发布候选和发布门禁; -后续公开需显式首发 Changeset 与验收,公开包不得依赖私有 workspace 包。 +后续公开需显式首发 Changeset 与验收。公开包的 `dependencies`、`optionalDependencies` +和 `peerDependencies` 不得引用私有 workspace 包;`devDependencies` 中的开发工具 +不构成安装消费者的依赖。 [PR #149](https://github.com/vooyajs/vooya/pull/149) 已将发布线工具和 Rust provider 抽离合入 main,并通过候选 CI。后续类型检查和测试工具整理留在 main,不扩大维护线。 diff --git a/tests/prepare-preset-consumer.mjs b/tests/prepare-preset-consumer.mjs index a5be50d2..c15740c6 100644 --- a/tests/prepare-preset-consumer.mjs +++ b/tests/prepare-preset-consumer.mjs @@ -2,8 +2,9 @@ // setup cache is distinct from the consumer's independently prepared cache. import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; +import { realpathSync } from "node:fs"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; +import { isAbsolute, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { prepareToolchain } from "../packages/preset/lib/index.js"; import { resolveToolchain } from "../packages/build-core/dist/index.js"; @@ -15,9 +16,11 @@ const prepared = await prepareToolchain({ cacheDirectory }); // prepared environment here; auto-discovery is tested separately in the app. const env = { ...prepared.environment, VOOYA_TOOLCHAIN: "system" }; const toolchain = resolveToolchain({ cwd: root, env }); -assert.equal(toolchain.cargo.path, prepared.cargoPath); -assert.ok(toolchain.rustc.path.startsWith(cacheDirectory)); -assert.ok(toolchain.wasmBindgen.path.startsWith(cacheDirectory)); +assert.equal(realpathSync.native(toolchain.cargo.path), realpathSync.native(prepared.cargoPath)); +for (const path of [toolchain.cargo.path, toolchain.rustc.path, toolchain.wasmBindgen.path]) { + const inside = relative(realpathSync.native(cacheDirectory), realpathSync.native(path)); + assert.ok(inside !== ".." && !inside.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(inside), "tool must resolve inside the setup cache"); +} const result = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build", "--workspace", "@vooya/core"], { cwd: root, env, stdio: "inherit", shell: process.platform === "win32", }); diff --git a/tests/preset-consumer.mjs b/tests/preset-consumer.mjs index ac78f6aa..943d1076 100644 --- a/tests/preset-consumer.mjs +++ b/tests/preset-consumer.mjs @@ -3,9 +3,9 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { createServer } from "node:net"; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, copyFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, copyFileSync, realpathSync } from "node:fs"; import { tmpdir } from "node:os"; -import { delimiter, dirname, join, resolve } from "node:path"; +import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { chromium } from "playwright"; import { startDevServer, stopDevServer } from "./helpers/dev-server.mjs"; @@ -15,6 +15,7 @@ const cacheDirectory = resolve(process.env.VOOYA_CACHE_DIR || join(root, "cache" const npm = process.platform === "win32" ? "npm.cmd" : "npm"; let server; let browser; +let page; let output = ""; let spawnError; function run(args, cwd, env = process.env) { @@ -36,7 +37,7 @@ try { for (const name of ["Counter.rs", "Counter.css", "Store.rs", "main.js"]) copyFileSync(join(repositoryRoot, "tests/fixtures/rust-vue/src", name), join(app, "src", name)); copyFileSync(join(repositoryRoot, "tests/fixtures/rust-vue/index.html"), join(app, "index.html")); writeFileSync(join(app, "vite.config.js"), 'import {defineConfig} from "vite"; import vue from "@vitejs/plugin-vue"; import {vooya} from "@vooya/vite"; export default defineConfig({plugins:[vue(),vooya({framework:"vue"})]});'); - writeFileSync(join(app, "src/App.vue"), ``); + writeFileSync(join(app, "src/App.vue"), ``); const blocked = join(root, "blocked-tools"); mkdirSync(blocked); for (const tool of ["cargo", "rustc", "rustup", "wasm-bindgen"]) { @@ -48,7 +49,7 @@ try { const ambient = spawnSync(tool, ["--version"], { env, encoding: "utf8", shell: process.platform === "win32" }); assert.equal(ambient.status, 97, `ambient ${tool} must be blocked`); } - writeFileSync(join(app, "probe.mjs"), `import assert from 'node:assert/strict'; import {prepareToolchain} from '@vooya/preset'; import {resolveToolchain} from '@vooya/build-core'; const [a,b]=await Promise.all([prepareToolchain(),prepareToolchain()]); assert.equal(a.cargoPath,b.cargoPath); const t=resolveToolchain({cwd:process.cwd(),env:process.env}); assert.equal(t.cargoSelection,'managed'); assert.equal(t.cargo.path,a.cargoPath); assert.ok(t.cargo.path.startsWith(process.env.VOOYA_CACHE_DIR)); assert.match(t.rustc.version,/1\\.94\\.0/); const explicit=resolveToolchain({cwd:process.cwd(),env:t.environment,mode:'system',cargoPath:t.cargo.path}); assert.equal(explicit.cargoSelection,'explicit'); console.log(JSON.stringify(t));`); + writeFileSync(join(app, "probe.mjs"), `import assert from 'node:assert/strict'; import {realpathSync} from 'node:fs'; import {relative,isAbsolute,sep} from 'node:path'; import {prepareToolchain} from '@vooya/preset'; import {resolveToolchain} from '@vooya/build-core'; const [a,b]=await Promise.all([prepareToolchain(),prepareToolchain()]); assert.equal(realpathSync.native(a.cargoPath),realpathSync.native(b.cargoPath)); const t=resolveToolchain({cwd:process.cwd(),env:process.env}); assert.equal(t.cargoSelection,'managed'); assert.equal(realpathSync.native(t.cargo.path),realpathSync.native(a.cargoPath)); for(const path of [t.cargo.path,t.rustc.path,t.wasmBindgen.path]) {const inside=relative(realpathSync.native(process.env.VOOYA_CACHE_DIR),realpathSync.native(path)); assert.ok(inside!=='..'&&!inside.startsWith('..'+sep)&&!isAbsolute(inside));} assert.match(t.rustc.version,/1\\.94\\.0/); const explicit=resolveToolchain({cwd:process.cwd(),env:t.environment,mode:'system',cargoPath:t.cargo.path}); assert.equal(explicit.cargoSelection,'explicit'); console.log(JSON.stringify(t));`); const probe = spawnSync(process.execPath, ["probe.mjs"], { cwd: app, env, encoding: "utf8" }); assert.equal(probe.status, 0, probe.stderr); const toolchain = JSON.parse(probe.stdout); @@ -56,8 +57,11 @@ try { assert.equal(doctor.ok, true, JSON.stringify(doctor)); assert.equal(doctor.schemaVersion, 1); assert.equal(doctor.cargo.selection, "managed"); - assert.equal(doctor.cargo.path, toolchain.cargo.path); - assert.ok(doctor.rustc.path.startsWith(cacheDirectory)); + assert.equal(realpathSync.native(doctor.cargo.path), realpathSync.native(toolchain.cargo.path)); + for (const path of [doctor.cargo.path, doctor.rustc.path, doctor.wasmBindgen.path]) { + const inside = relative(realpathSync.native(cacheDirectory), realpathSync.native(path)); + assert.ok(inside !== ".." && !inside.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(inside), "doctor tool must resolve inside the consumer cache"); + } const port = await availablePort(); server = startDevServer(npm, ["run", "dev", "--", "--host", "127.0.0.1", "--port", String(port), "--strictPort"], { cwd: app, env, shell: process.platform === "win32", stdio: ["ignore", "pipe", "pipe"] }); server.on("error", error => { spawnError = error; }); @@ -66,7 +70,9 @@ try { const url = `http://127.0.0.1:${port}`; await waitUntil(async () => { if (spawnError) throw spawnError; assert.equal(server.exitCode, null, output); assert.equal(server.signalCode, null, output); try { return (await fetch(url, { signal: AbortSignal.timeout(1000) })).ok; } catch { return false; } }, "dev server startup"); browser = await chromium.launch({ headless: true }); - const page = await browser.newPage(); + page = await browser.newPage(); + page.on("pageerror", error => { output += `\nBrowser error: ${error.message}`; }); + page.on("console", message => { if (message.type() === "error") output += `\nBrowser console: ${message.text()}`; }); page.setDefaultTimeout(120_000); await page.goto(url); await page.getByRole("button", { name: "Count: 0", exact: true }).waitFor(); @@ -90,6 +96,14 @@ try { run(["run", "build"], app, env); console.log(`Managed packed consumer passed (${process.platform}/${process.arch}): ordinary dev/browser Component+Store, Rust edit, compiler error/recovery, production build, concurrent cache and doctor; ambient Rust commands blocked, host SDK retained.`); } catch (error) { + if (page && !page.isClosed()) { + try { + console.error("Failed page:", await page.locator("body").innerText({ timeout: 1000 })); + const screenshot = join(tmpdir(), `vooya-preset-failure-${Date.now()}.png`); + await page.screenshot({ path: screenshot, timeout: 5000 }); + console.error(`Failure screenshot: ${screenshot}`); + } catch (captureError) { console.error("Could not capture failed page:", captureError); } + } if (output) console.error(output); throw error; } finally { From be1fb582951d473e62c16b920228c4cfc690d827 Mon Sep 17 00:00:00 2001 From: Carbon Date: Mon, 5 Oct 2026 19:13:14 +0800 Subject: [PATCH 3/3] fix(test): canonicalize Windows consumer roots --- tests/preset-consumer.mjs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/preset-consumer.mjs b/tests/preset-consumer.mjs index 943d1076..69cd0e2e 100644 --- a/tests/preset-consumer.mjs +++ b/tests/preset-consumer.mjs @@ -9,7 +9,8 @@ import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:pa import { fileURLToPath } from "node:url"; import { chromium } from "playwright"; import { startDevServer, stopDevServer } from "./helpers/dev-server.mjs"; -const root = mkdtempSync(join(tmpdir(), "vooya-preset-consumer-")); +// Windows temp paths may use an 8.3 alias; Vite compares requests with real paths. +const root = realpathSync.native(mkdtempSync(join(tmpdir(), "vooya-preset-consumer-"))); const repositoryRoot = fileURLToPath(new URL("..", import.meta.url)); const cacheDirectory = resolve(process.env.VOOYA_CACHE_DIR || join(root, "cache")); const npm = process.platform === "win32" ? "npm.cmd" : "npm"; @@ -68,7 +69,19 @@ try { server.stdout.on("data", data => { output += data; }); server.stderr.on("data", data => { output += data; }); const url = `http://127.0.0.1:${port}`; - await waitUntil(async () => { if (spawnError) throw spawnError; assert.equal(server.exitCode, null, output); assert.equal(server.signalCode, null, output); try { return (await fetch(url, { signal: AbortSignal.timeout(1000) })).ok; } catch { return false; } }, "dev server startup"); + await waitUntil(async () => { + if (spawnError) throw spawnError; + assert.equal(server.exitCode, null, output); + assert.equal(server.signalCode, null, output); + let response; + try { response = await fetch(url, { signal: AbortSignal.timeout(1000) }); } + catch { return false; } // The listening socket may not exist yet. + if (response.status >= 400 && response.status < 500) { + const body = await response.text().catch(error => ``); + throw new Error(`Dev server startup returned HTTP ${response.status}: ${body}`); + } + return response.ok; + }, "dev server startup"); browser = await chromium.launch({ headless: true }); page = await browser.newPage(); page.on("pageerror", error => { output += `\nBrowser error: ${error.message}`; });