Skip to content

test(vue): add Vite SSR example - #2033

Open
eoinest wants to merge 3 commits into
e/vue/add-vue-vite-examplefrom
e/vue/add-spa-ssr-examples
Open

test(vue): add Vite SSR example#2033
eoinest wants to merge 3 commits into
e/vue/add-vue-vite-examplefrom
e/vue/add-spa-ssr-examples

Conversation

@eoinest

@eoinest eoinest commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add a Vite 8 / Vue 3.5 / Vue Router 5 SSR documentation-shell example using linked gt-vue packages and local catalogs.
  • Demonstrate fresh per-request translation state across SSR, hydration, lazy routes, scoped slots, Suspense, Transition, Teleport, formatters, msg(), Vue TSX, and locale-prefixed navigation.
  • Add the SPA and SSR examples to the persistent Chromium app matrix, fail Vue runs on browser warnings, and build gt-vue in the scheduled workflow.

Testing

  • pnpm install --frozen-lockfile — passed
  • pnpm --filter vite-vue-ssr generate — passed (33 extracted entries, stable across repeated generation)
  • pnpm --filter vite-vue-ssr validate — passed
  • pnpm --filter vite-vue-ssr typecheck — passed
  • pnpm --filter vite-vue-ssr test — passed (2 tests, including 128 overlapping English/French requests)
  • pnpm --filter vite-vue-ssr build — passed (client and SSR production bundles)
  • GT_TEST_APPS=vite-vue,vite-vue-ssr pnpm --filter gt-test-apps-e2e test:e2e — passed in Chromium
  • pnpm --filter gt-test-apps-e2e lint — passed
  • pnpm --filter gt-test-apps-e2e typecheck — passed

Notes

Greptile Summary

This PR adds a Vite 8 / Vue 3.5 / Vue Router 5 SSR documentation-shell example (vite-vue-ssr) that demonstrates per-request translation state isolation, client-side hydration, lazy routes, scoped slots, Suspense, Transition, Teleport, formatters, msg(), and Vue TSX compatibility. It also wires both Vue examples into the persistent Chromium E2E matrix and adds gt-vue to the scheduled cron build.

  • SSR isolation: every call to render() constructs a fresh GTPlugin, router, and Vue app instance, verified by 128 concurrent overlapping English/French renders in the unit test suite and 8 parallel requests in the E2E test.
  • Client hydration: locale is serialised into a type="application/json" script tag with \u003c-escaped JSON and read back in entry-client.ts with a URL-derived fallback, preventing mismatch on initial paint.
  • Fixture update: Vue console warnings are now treated as test failures for vite-vue* apps via an additive condition that does not alter behaviour for existing non-Vue tests.

Confidence Score: 5/5

  • This PR is safe to merge — it adds a self-contained example package and test infrastructure with no changes to any published library code.
  • All changes are confined to a new private example app, its unit and E2E tests, and CI workflow additions. The SSR isolation pattern is sound (fresh plugin instance per request), escaping is applied correctly in both HTML-attribute and JSON-in-script contexts, and the concurrent-render test suite directly validates the isolation guarantee.
  • No files require special attention.

Important Files Changed

Filename Overview
examples/vite-vue-ssr/src/entry-server.ts Stateless render function that creates a fresh GT plugin and app per request, with a clear JSDoc explaining the isolation contract. No shared mutable state.
examples/vite-vue-ssr/src/app.ts Shared app factory for both SSR and client; registers a beforeResolve guard to sync locale on navigation, then explicitly loads translations after the router is ready.
examples/vite-vue-ssr/server.ts Express SSR server with dev/prod branching; locale value is HTML-escaped before injection into the lang attribute and JSON is escaped with \u003c to prevent script-injection.
examples/vite-vue-ssr/src/entry-client.ts Client hydration bootstrap; reads locale from the injected #gt-state JSON script tag with a safe try/catch fallback to URL-derived locale before mounting.
examples/vite-vue-ssr/src/entry-server.test.ts Unit tests for SSR isolation — verifies correct locale in rendered HTML and runs 128 concurrent overlapping English/French renders to validate per-request state isolation.
tests/apps/test-apps-e2e/e2e/fixtures.ts Extends the error-collection fixture to treat Vue console warnings as test failures for vite-vue apps; logic is correct and backward-compatible with non-Vue test apps.
tests/apps/test-apps-e2e/e2e/app.spec.ts Adds testVueSpa and testVueSsr E2E test paths covering SSR HTML content, locale isolation under 8 parallel requests, hydration, locale switching via navigation, dialog interactions, and page reload.
examples/vite-vue-ssr/src/router.ts Simple two-route router with locale-prefixed paths; getLocaleFromUrl uses URL constructor for safe parsing and handles both absolute URLs and relative paths correctly.
examples/vite-vue-ssr/src/views/ReferencePage.vue Exercises onServerPrefetch, Plural, Num, DateTime, and Var components; onServerPrefetch resolves immediately as a demo stub for the SSR lifecycle hook.
tests/apps/test-apps-e2e/apps.mjs Adds vite-vue (SPA) and vite-vue-ssr entries to the persistent Chromium app matrix with correct ports and ready-check paths.
.github/workflows/test-apps-e2e-cron.yml Adds gt-vue to the scheduled build step so the Vue package is compiled before E2E tests run against it in the cron workflow.

Sequence Diagram

sequenceDiagram
    participant B as Browser
    participant S as server.ts (Express)
    participant ES as entry-server.ts
    participant A as app.ts
    participant R as router.ts
    participant T as translations.ts
    participant VR as vue/server-renderer

    B->>S: GET /fr/reference
    S->>ES: render('/fr/reference')
    ES->>R: getLocaleFromUrl('/fr/reference') → 'fr'
    ES->>A: createDocsGT('fr')
    ES->>A: createDocsApp('/fr/reference', true, gt)
    A->>R: "createDocsRouter(server=true)"
    A->>R: router.push('/fr/reference')
    Note over A,R: beforeResolve guard fires
    A->>A: gt.setLocale('fr')
    A->>T: loadTranslations('fr')
    T-->>A: fr.json catalog
    A-->>ES: "{ app, gt, router }"
    ES->>VR: renderToString(app, context)
    VR-->>ES: "{ html, teleports }"
    ES-->>S: "{ html, locale:'fr', teleports }"
    S->>S: inject locale/html/teleports/state into index.html
    S-->>B: 200 text/html (SSR-rendered French page)

    Note over B: Hydration
    B->>B: "readState() → { locale: 'fr' } from #gt-state"
    B->>A: createDocsGT('fr')
    B->>A: createDocsApp(url, false, gt)
    B->>B: "app.mount('#app')"
    B->>B: "data-hydrated='true'"
Loading

Reviews (8): Last reviewed commit: "test(vue): validate TSX and per-request ..." | Re-trigger Greptile

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
generaltranslation 18.09 KB (0%)
generaltranslation/runtime 14.87 KB (0%)
generaltranslation/id 2.55 KB (0%)
generaltranslation/internal 7.36 KB (0%)
generaltranslation/types 115 B (0%)
generaltranslation/errors 81 B (0%)
@generaltranslation/format 9.5 KB (0%)
@generaltranslation/format/types 89 B (0%)
@generaltranslation/format/internal 880 B (0%)
gt-i18n 11.96 KB (0%)
gt-i18n/types 13 B (0%)
gt-i18n/internal 22.4 KB (0%)
gt-i18n/internal/cookies 228 B (0%)
gt-i18n/internal/string 2.9 KB (0%)
gt-i18n/internal/types 13 B (0%)
@generaltranslation/react-core/pure 25.6 KB (0%)
@generaltranslation/react-core/hooks 20.6 KB (0%)
@generaltranslation/react-core/components 22.74 KB (0%)
@generaltranslation/react-core/components-rsc 26.19 KB (0%)
gt-react (client) 32.14 KB (0%)
gt-react (rsc) 28.71 KB (0%)
gt-react (server) 31.77 KB (0%)
gt-react/macros 8.81 KB (0%)
gt-vue 8.18 KB (0%)
gt-next (client) 44.2 KB (0%)
gt-next (rsc) 48.16 KB (0%)
gt-next (server) 44.31 KB (0%)
gt-next/config 270.72 KB (0%)
gt-next/server 46.71 KB (0%)
gt-next/middleware 36.82 KB (0%)
gt-next/link 43.02 KB (0%)
gt-next/internal/_dictionary 144 B (0%)
gt-next/internal/_load-translations 144 B (0%)
gt-next/internal/_load-dictionary 144 B (0%)
gt-next/internal/_getLocale 125 B (0%)
gt-next/internal/_getRegion 122 B (0%)
gt-node 23.61 KB (0%)
gt-node/types 219 B (0%)
gt-node/internal 13.43 KB (0%)
gt-tanstack-start (client) 31.68 KB (0%)
gt-tanstack-start (server) 32.13 KB (0%)
gt-tanstack-start/server 10.27 KB (0%)
gt-react-native 30.11 KB (0%)
gt-react-native/plugin 4.6 KB (0%)
gt-react-native/internal 746 B (0%)

Comment thread examples/vite-vue-ssr/src/App.vue
Comment thread examples/vite-vue-ssr/src/entry-server.ts Outdated
@eoinest
eoinest changed the base branch from e/vue/fix-formatter-hydration to e/vue/add-vue-vite-example August 6, 2026 00:13
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

1 similar comment
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread examples/vite-vue-ssr/server.ts Fixed
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

1 similar comment
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest
eoinest force-pushed the e/vue/add-spa-ssr-examples branch from c5a5fb7 to dde1fee Compare August 6, 2026 22:29
@eoinest

eoinest commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

@eoinest

eoinest commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

@greptileai

eoinest added a commit that referenced this pull request Aug 7, 2026
## Summary

- Add an unstable, lightweight `gt-vue` runtime with `T`, child-only
`Var`, typed-value `Num` / `DateTime` / `Currency`, `Plural`, `Branch`,
`useGT`, `useMessages`, and `msg`.
- Keep string translation deliberately narrow: `$context` is the only
public metadata option; all content is literal STRING data with no ICU
interpolation, formatting fields, or development hot reload.
- Provide isolated `createGT()` instances, async per-locale catalog
caching, reactive locale changes, latest-wins concurrency, retryable
loader diagnostics, and source-locale fast paths for client and SSR use.
- Preserve Vue component props/listeners and scoped slots, keep
rich-content IDs and plural selection compatible with react-core,
exclude presentation/runtime attributes from persisted Branch hashes,
and suppress unsupported fallthrough attrs on wrapperless value
components.
- Share the literal STRING hash/registration contract through
`gt-i18n/internal/string`; `gt-i18n` is a real external runtime
dependency in both ESM and CJS.

## Testing

- `gt-vue`: full runtime suite passed, including SSR/hydration, keyed
VNode reconciliation, opaque slots, Suspense, and concurrency
- `gt-i18n`: 33 files / 314 tests
- `@generaltranslation/react-core`: 12 files / 50 tests, including
shared hand-pinned wire-format fixtures
- Forced Turbo build/typecheck for the runtime dependency graph — passed
- Root library-default, lint, and format gates — passed
- Size limits — passed (`gt-vue` 7.56 kB Brotli;
`gt-i18n/internal/string` 2.97 kB)
- Packed consumers on Vue 3.3.13 and 3.5.40 — passed ESM, CJS, SSR,
`vue-tsc`, Vite production builds, and real Chromium locale/formatting
loops with zero console warnings/errors
- Changesets simulation — `gt-vue@0.1.0` correctly depends on
`gt-i18n@1.1.0`

## Notes

- `gt-vue` is explicitly documented as unstable and independently
versioned during 0.x.
- Changesets: minor for both the first `gt-vue` release and the literal
STRING contract update in `gt-i18n`.
- Stack 1/4. Base: `main`. Next: #2013, #2014, then #2033.
- This PR is not being merged or published yet.

<!-- greptile_comment -->

<h3>Greptile Summary</h3>

This PR introduces `gt-vue`, a new lightweight Vue 3 i18n runtime
package, plus a shared `gt-i18n/internal/string` contract for
STRING-format translation hashing and cookie utilities consumed by both
Vue and React runtimes. It also refactors `gt-i18n`'s `msg` to delegate
to a new `msgString` path when `$format: 'STRING'` is requested, and
consolidates cookie-name constants from `react-core` into `gt-i18n`.

- **`gt-vue` runtime**: adds `T`, `Var`, `Num`, `DateTime`, `Currency`,
`Plural`, `Branch`, `createGT`, `useGT`, `useMessages`, `useLocale`,
`useSetLocale`, and `msg` — with reactive locale state, per-locale
catalog caching, latest-wins concurrency, SSR/hydration support, and
source-locale fast paths.
- **`gt-i18n` additions**: `msgString`, `encodeMsg`,
`hashStringMessage`, browser cookie utilities, and new internal entry
points (`internal-string`, `internal-cookies`) shared across frameworks.
- **`react-core` cleanup**: `cookieNames.ts` now re-exports from
`gt-i18n/internal/cookies` rather than duplicating constants.

<details open><summary><h3>Confidence Score: 5/5</h3></summary>

- This PR is safe to review and merge — no functional regressions or
data-loss paths were found across the new Vue runtime, the shared
gt-i18n STRING contract, or the react-core consolidation.
- The new gt-vue package adds a large surface area (rich-content
rendering, locale concurrency, SSR/hydration, cookie management) but
each subsystem is self-contained and backed by targeted tests. The
latest-wins concurrency logic in setLocale, the revision-counter
reactivity contract, the source-locale fast-path that deliberately skips
catalog reads, the VNode identity cache rollback on render errors, and
the `isEncodedTranslationOptions` type guard before msg decode are all
implemented correctly. The react-core cookie constant consolidation is a
straight re-export with no behavioral change. No blocking issues were
found.
- No files require special attention.
</details>

<details><summary><h3>Important Files Changed</h3></summary>

| Filename | Overview |
|----------|----------|
| packages/vue/src/runtime/state.ts | Core plugin factory. Locale
concurrency (latest-wins), per-locale catalog caching, cookie-backed
locale persistence, and reactive revision counter are all implemented
correctly. `console.error` on load failure is intentional diagnostic
output, not debug logging. |
| packages/vue/src/rendering/translateVueChildren.ts | Rich-content
translation engine. Source serialization, hash-based catalog lookup,
variable/branch/plural rendering, VNode identity caching with rollback,
and metadata copying all look correct. `copyRenderMetadata` relies on
undocumented `ctx` / `slotScopeIds` fields (flagged in a previous review
thread). `cloneWithProps` for components intentionally passes a VNode to
`h()` to take the clone-and-renormalize path. |
| packages/vue/src/runtime/localeCookie.ts | Cookie-backed locale
accessor. SSR/hydration contract is correct: explicit locale overrides a
stale browser cookie on first mount, `getLocale` reads live cookie on
browser, `serverLocale` closure isolates per-request SSR state. |
| packages/vue/src/composables/strings.ts | useGT and useMessages
composables. Encoded msg metadata is decoded and short-circuited through
the precomputed hash path; raw strings fall through to the regular
STRING hash lookup. Nullish passthrough is correct. |
| packages/vue/src/components/variables.ts | Num, DateTime, Currency,
and Var components. Null/empty-string guards, NaN fallback to
String(value), and _locale override for the rich-translation pipeline
are all correctly implemented. |
| packages/vue/src/components/branches.ts | Plural and Branch
components. getBranchNames deduplicates attr/slot names via Set
(regression test included). getBranchContent slot-wins logic and
getFormatLocales fallback are correct. |
| packages/i18n/src/translation-functions/msg/msgString.ts | New
STRING-format msg registration path. Array recursion with $id indexing,
STRING format tagging, and encodeMsg delegation are all correct. The
early return when options is absent preserves the original input's type.
|
| packages/i18n/src/utils/hashStringMessage.ts | Canonical STRING
catalog key computation. Precomputed $_hash short-circuit is used
correctly by both msgString and translateString to avoid re-hashing. |
| packages/react-core/src/setup/cookieNames.ts | Replaced duplicated
cookie constants with re-exports from gt-i18n/internal/cookies. Single
source of truth for cookie names across runtimes. |
| packages/vue/src/components/utils.ts | Shared component utilities.
isBranchAttribute correctly excludes event handlers, ARIA/data
attributes, and the NON_BRANCH_ATTRIBUTE_NAMES set. getBranchNames
deduplication and getBranchContent slot-priority logic look correct. |

</details>

<details><summary><h3>Sequence Diagram</h3></summary>

```mermaid
sequenceDiagram
    participant App as Vue App
    participant Plugin as GTPlugin (createGT)
    participant State as GTState
    participant Cookie as CookieBackedLocale
    participant Loader as loadTranslations
    participant Catalog as catalogs Map
    participant T as T Component
    participant Renderer as translateVueChildren

    App->>Plugin: app.use(plugin)
    Plugin->>State: provide(gtContextKey, state)
    Plugin->>Loader: load(getLocale()) [async, fire-and-forget]
    Loader-->>Catalog: catalogs.set(locale, catalog)
    Loader-->>State: "revision.value += 1"

    App->>Plugin: setLocale('fr')
    Plugin->>Loader: load('fr')
    note over Plugin,Loader: concurrent requests share one Promise<br/>latest-wins: older requests return early
    Loader-->>Catalog: catalogs.set('fr', catalog)
    Plugin->>Cookie: setLocale('fr') → writes cookie
    Plugin-->>State: "revision.value += 1"

    T->>State: getLocale() → reads revision (reactive)
    T->>State: getCatalog() → reads revision (reactive)
    T->>Renderer: translateVueChildren(vnodes, state, options)
    Renderer->>Renderer: createSourceNodes → serializeNodes → hashSource
    Renderer->>Catalog: getCatalog()[hash]
    Renderer-->>T: translated VNodeChild
```
</details>

<sub>Reviews (13): Last reviewed commit: ["fix(vue): persist locale in
browser
cook..."](163973a)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=48899958)</sub>

<!-- /greptile_comment -->
@eoinest
eoinest force-pushed the e/vue/add-spa-ssr-examples branch from 714a960 to 3b4d50c Compare August 7, 2026 00:53
@eoinest
eoinest force-pushed the e/vue/add-spa-ssr-examples branch from 3b4d50c to ca3be55 Compare August 7, 2026 02:19
@eoinest
eoinest force-pushed the e/vue/add-spa-ssr-examples branch from ca3be55 to a6835d5 Compare August 7, 2026 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants