Skip to content

feat(examples): add Vite SPA quickstart example - #1943

Merged
JoshKappler merged 7 commits into
mainfrom
feat/example-vite-spa
Jul 22, 2026
Merged

feat(examples): add Vite SPA quickstart example#1943
JoshKappler merged 7 commits into
mainfrom
feat/example-vite-spa

Conversation

@JoshKappler

@JoshKappler JoshKappler commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Addition, take or leave. One of five SPA example PRs from this morning's list (webpack #1944, rollup #1945, react-router #1946, rsbuild #1951 on top of the compiler adapter #1950).

Summary

  • New example examples/vite-spa: the finished product of the React SPA Quickstart plus the "Developing with SPA translations" guide, built to follow the docs faithfully so docs and example can't drift apart.
  • Runtime story: src/index.ts awaits initializeGTSPA then dynamically imports the app, so module-level t() resolves. No provider. <T>, t(), and <LocaleSelector> all demonstrated.
  • Buildtime story: the compiler's vite plugin with parsingFlags.devHotReload, matching vite-create-app.
  • Hand-written translation files under src/_gt/ are keyed by the real content hashes, so language switching works offline with no account or API key. npm run build runs offline; a separate translate script regenerates the files with a production key.
  • vercel.json ships an SPA fallback rewrite so deployed deep links and locale-switch reloads resolve.

Testing

  • typecheck, oxlint, offline build, and a frozen-lockfile install all pass on the current head.
  • Injected _hash values in the built bundle match the committed fixture keys, with the compiler on and with it removed (runtime hashing computes the same keys).
  • Headless Chromium: switching the LocaleSelector rendered en, es, and ja correctly with zero console errors.
  • CI lints examples but does not build them, so the build evidence above is from manual runs.

Notes

  • This is a second Vite example on purpose: vite-create-app shows GT retrofitted onto a create-vite scaffold, vite-spa is the from-scratch docs companion. Both READMEs say which to use. If one Vite example is enough, happy to fold or drop this one.
  • Docs gap found while building, filed separately: the dev guide never mentions parsingFlags.devHotReload, but module-level t() strings won't hot-reload without it.
  • No changeset (example only, matches fix(examples): restore Vercel workspace deployments #1908/chore(examples): update vite #1927).

Greptile Summary

This PR adds a new examples/vite-spa example — the finished product of the React SPA Quickstart — demonstrating gt-react in a client-side-only Vite app. The runtime story uses top-level await initializeGTSPA() before dynamically importing main.tsx, so module-level t`...` calls in navigation.ts resolve correctly. Hand-written translation fixture files in src/_gt/ enable offline switching across four locales without an API key.

  • src/index.ts: Imports gt-react/macros, awaits initializeGTSPA (passing config + env vars), then dynamically imports ./main — ensuring GT is fully initialized before the component tree renders.
  • src/loadTranslations.ts: Dynamic-imports the per-locale JSON at runtime, returning {} on any missing locale so the app degrades gracefully.
  • Root .oxlintrc.json: Adds t as a global readonly identifier so the monorepo-wide no-undef rule does not flag the implicit t macro in navigation.ts.

Confidence Score: 5/5

Safe to merge. The new example is a self-contained addition with no changes to production library code, and the one shared-config tweak is additive.

All source changes are confined to the new examples/vite-spa directory. The only repo-wide modification is adding t to the oxlint globals block, which is additive and doesn't affect runtime behavior or library packages. The initialization sequence (macros → initializeGTSPA → dynamic import of main) is architecturally sound, offline build is verified, and the lockfile correctly reflects the package.json dependency placement.

.oxlintrc.json — the t global is broader than needed; worth scoping to SPA examples via an override, but not a blocker.

Important Files Changed

Filename Overview
examples/vite-spa/src/index.ts Entry module: correctly sequences macro import, initializeGTSPA await, and dynamic app import to guarantee GT is ready before any module-level t`` calls execute.
examples/vite-spa/src/navigation.ts Uses implicit global t`` macro (attached by gt-react/macros in index.ts); intentional SPA-only pattern, well-documented in README.
examples/vite-spa/src/loadTranslations.ts Dynamic-imports locale JSON from src/_gt/; returns {} on missing locale. console.warn is explicitly allowed by the no-console lint rule.
.oxlintrc.json Adds 't' as a monorepo-wide readonly global so CI no-undef checks pass for navigation.ts; broader than strictly necessary since t is only a valid implicit global in SPA entry contexts.
examples/vite-spa/vite.config.ts Wires react() and gtCompiler(gtConfig) plugins; dedupe for react/react-dom handles workspace-link resolution correctly.
examples/vite-spa/vercel.json Uses turbo build command with --env-mode=loose; includes SPA fallback rewrite; matches conventions of sibling examples.
examples/vite-spa/package.json gt is correctly placed under devDependencies (matches its lockfile placement); gt-react in dependencies; all workspace:* pins consistent.
examples/vite-spa/gt.config.json Configures defaultLocale, four target locales, output path, and parsingFlags.devHotReload for hot-reload of module-level t`` strings during development.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant index.ts
    participant loadTranslations.ts
    participant _gt/[locale].json
    participant initializeGTSPA
    participant main.tsx
    participant App

    Browser->>index.ts: load (module script)
    index.ts->>index.ts: import 'gt-react/macros' (attaches global t``)
    index.ts->>initializeGTSPA: "await initializeGTSPA({ ...gtConfig, projectId, devApiKey, loadTranslations })"
    initializeGTSPA->>loadTranslations.ts: call loadTranslations(locale)
    loadTranslations.ts->>_gt/[locale].json: dynamic import
    _gt/[locale].json-->>loadTranslations.ts: translation map
    loadTranslations.ts-->>initializeGTSPA: resolved translations
    initializeGTSPA-->>index.ts: resolved
    index.ts->>main.tsx: await import('./main')
    main.tsx->>App: createRoot().render(App)
    App->>App: navigation uses t`Home`, t`About` (resolved)
    App->>App: Welcome renders T and LocaleSelector
Loading

Reviews (7): Last reviewed commit: "fix(examples): use the global t macro an..." | Re-trigger Greptile

Comment thread pnpm-lock.yaml
@JoshKappler

Copy link
Copy Markdown
Contributor Author

Folded into #1949 along with the other three SPA examples to keep the review in one place. The commit and write-up carry over unchanged.

Bring examples/vite-spa to the reviewed final state from the combined
spa-examples PR (commit 627189a).

- vercel.json: add the SPA fallback rewrite so client-side routes serve
  index.html instead of 404ing on deep links.
- pnpm-lock.yaml: regenerate from the reviewed combined lockfile. The
  branch's prior lockfile was stale (its vite-spa entry no longer matched
  the manifest and failed a frozen install). Seeding from the reviewed
  lockfile keeps webpack deduplicated to a single instance, which the
  naive regeneration split into two peer-context variants and broke the
  gt-test-webpack-react build.
@JoshKappler JoshKappler reopened this Jul 20, 2026
@JoshKappler

Copy link
Copy Markdown
Contributor Author

@greptileai review

@JoshKappler

Copy link
Copy Markdown
Contributor Author

@greptileai review

@JoshKappler

Copy link
Copy Markdown
Contributor Author

@greptileai review

JoshKappler added a commit that referenced this pull request Jul 21, 2026
Addition, take or leave. One of five SPA example PRs from this morning's
list (vite #1943, rollup #1945, react-router #1946, rsbuild #1951 on top
of the compiler adapter #1950).

## Summary

- New example `examples/webpack-spa`: plain React 19 bundled with
webpack 5, no framework, client side only.
- Runtime story: `initializeGTSPA` entry module (no provider), six `<T>`
blocks including a `<Var>` runtime value, one module-level `t()`,
`<LocaleSelector>`.
- Buildtime story: the compiler's `webpack` export as the first plugin.
Credentials come in through dotenv plus DefinePlugin, since webpack has
no `import.meta.env`, and production builds inline empty strings so no
key ever lands in a production bundle.
- Hand-written fixtures under `src/_gt/` keyed by real content hashes;
locale switching works offline. `npm run build` runs offline; a separate
`translate` script regenerates the files with a production key.

## Testing

- typecheck, oxlint, offline production build, frozen-lockfile install:
all pass on the current head. All 7 injected `_hash` values match the
fixture keys; verified in a real browser across all five locales.
- Grepped the production bundle with a planted `GT_DEV_API_KEY` set: the
value does not appear (dev builds embed it by design, that is the
documented dev workflow on every bundler).
- The internal `tests/apps/webpack-react` build passes on this branch
(see the postcss note below).
- CI lints examples but does not build them, so the build evidence is
from manual runs.

## Notes

- One non-example change, disclosed plainly: `postcss` is pinned in the
workspace root devDependencies. Without it, this example's css-loader
pulls postcss into the dependency graph and splits webpack into two
peer-keyed instances, which breaks the `tests/apps/webpack-react` build
with an instanceof error. The same line ships in #1945 (its postcss
plugin has the same effect); whichever merges first carries it and the
other becomes a no-op. If there's a preferred way to manage the peer
graph, happy to redo this part.
- A CodeQL alert flags the dev-key DefinePlugin line. Production builds
provably embed nothing (see Testing); dev builds embedding the dev key
is the documented design, so the alert needs a dismissal rather than a
code change.
- `vercel.json` sets `outputDirectory: dist` (webpack gets no Vercel
preset) plus an SPA fallback rewrite.
- No changeset (example only).

<!-- greptile_comment -->

<h3>Greptile Summary</h3>

This PR adds a new `examples/webpack-spa` directory: a plain React 19
SPA bundled with webpack 5, demonstrating the GT i18n runtime and
compiler without any framework. A single non-example change pins
`postcss` at the workspace root to prevent a peer-dependency split that
broke the existing `tests/apps/webpack-react` build.

- **New example**: `initializeGTSPA` entry pattern with top-level
`await`, six `<T>` blocks (including a `<Var>` runtime value), one
module-level `t()`, and `<LocaleSelector>` — all exercised against
hand-written fixture files in `src/_gt/` that enable offline locale
switching.
- **Credential handling**: `dotenv` + `DefinePlugin` replaces
`import.meta.env` (unavailable in webpack); production builds
unconditionally inline empty strings so no API key ever lands in a
production bundle.
- **Workspace fix**: `postcss@^8.5.19` pinned at the repo root resolves
a duplicate peer-keyed instance that caused an `instanceof` error in
`tests/apps/webpack-react`.

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

Safe to merge — this is a new example directory plus a targeted
workspace-root dependency pin with no changes to library source code.

All changes are additive: a self-contained example app and a single
postcss pin that fixes an existing peer-dependency conflict. The
credential-handling logic is correct (production builds embed empty
strings), the GT initialization pattern matches the documented SPA
quickstart, and the hand-written fixtures are valid. No library code is
modified.

No files require special attention.
</details>

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

| Filename | Overview |
|----------|----------|
| examples/webpack-spa/webpack.config.mjs | Webpack config with GT
compiler integration, DefinePlugin for credentials, and react alias for
monorepo isolation — correctly gates API keys to dev builds only |
| examples/webpack-spa/src/index.ts | Top-level-await entry module that
initializes GT before dynamically importing the React app, ensuring
module-level t() calls resolve correctly |
| examples/webpack-spa/src/loadTranslations.ts | Dynamic locale import
with graceful fallback to empty object on missing files; webpack context
module will bundle all _gt/*.json at build time |
| examples/webpack-spa/src/App.tsx | Demo component exercising T, Var,
t(), useLocale, and LocaleSelector with a module-level t() call that
relies on GT being initialized before import |
| package.json | Pins postcss@^8.5.19 at the workspace root to prevent
duplicate peer-keyed postcss instances from breaking the
tests/apps/webpack-react build |
| examples/webpack-spa/vercel.json | Vercel deployment config with turbo
build command, frozen-lockfile install, correct outputDirectory, and SPA
catch-all rewrite |

</details>

<details><summary><h3>Flowchart</h3></summary>

<a href="#gh-light-mode-only">

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["Browser loads index.html"] --> B["webpack bundle executes src/index.ts"]
    B --> C["dotenv values injected via DefinePlugin\n(dev: from .env.local, prod: empty strings)"]
    C --> D["initializeGTSPA()\nawait — blocks until GT is ready"]
    D --> E{Locale?}
    E -->|non-default| F["loadTranslations(locale)\nimport('./_gt/locale.json')"]
    E -->|default/en| G["No translation file needed"]
    F --> H["webpack context module\nbundles all _gt/*.json at build time"]
    H --> I["translations returned to GT runtime"]
    G --> I
    I --> J["await import('./main')\nReact app mounts AFTER GT init"]
    J --> K["App.tsx renders\n<T>, t(), <Var>, <LocaleSelector>"]
    K --> L["Module-level t() resolves correctly\n(GT already initialized)"]
```

</a>
<a href="#gh-dark-mode-only">

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["Browser loads index.html"] --> B["webpack bundle executes src/index.ts"]
    B --> C["dotenv values injected via DefinePlugin\n(dev: from .env.local, prod: empty strings)"]
    C --> D["initializeGTSPA()\nawait — blocks until GT is ready"]
    D --> E{Locale?}
    E -->|non-default| F["loadTranslations(locale)\nimport('./_gt/locale.json')"]
    E -->|default/en| G["No translation file needed"]
    F --> H["webpack context module\nbundles all _gt/*.json at build time"]
    H --> I["translations returned to GT runtime"]
    G --> I
    I --> J["await import('./main')\nReact app mounts AFTER GT init"]
    J --> K["App.tsx renders\n<T>, t(), <Var>, <LocaleSelector>"]
    K --> L["Module-level t() resolves correctly\n(GT already initialized)"]
```

</a>
</details>

<sub>Reviews (5): Last reviewed commit: ["fix(examples): correct
monorepo install
..."](c959f60)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=45751556)</sub>

<!-- /greptile_comment -->
@JoshKappler

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread examples/vite-spa/src/navigation.ts Outdated
@@ -0,0 +1,6 @@
import { t } from 'gt-react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can actually do better here. in index.ts add import "gt-react/macros". This will attach the t tagged template literal to the global scope. We can then get away without needing to import the t` every in every file. So this file becomes just the following:

export const navigation = [
  { label: t`Home`, href: '/' },
  { label: t`About`, href: '/about' },
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi this is for SPAs only

@eoinest eoinest left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left one nit

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe gt logo?

@JoshKappler

Copy link
Copy Markdown
Contributor Author

@greptileai review

@JoshKappler
JoshKappler merged commit 92d1588 into main Jul 22, 2026
30 checks passed
@JoshKappler
JoshKappler deleted the feat/example-vite-spa branch July 22, 2026 22:27
JoshKappler added a commit that referenced this pull request Jul 22, 2026
Addition, take or leave. One of five SPA example PRs from this morning's
list (vite #1943, webpack #1944, react-router #1946, rsbuild #1951 on
top of the compiler adapter #1950).

## Summary

- New example `examples/rollup-spa`: plain React 19 bundled with pure
Rollup, wired from the proven `tests/apps/rollup-react` config
(compiler's `rollup` export first, then replace, nodeResolve, commonjs,
json, typescript, postcss, and a small guarded html emitter with a
single shared CSS path constant).
- Runtime story: `initializeGTSPA` entry module, `<T>`, `<Var>`,
module-level `t()`, `<LocaleSelector>`.
- `loadTranslations` uses an explicit per-locale static import map
instead of the docs' interpolated dynamic import, because Rollup can't
code-split a fully dynamic path. Each locale still gets its own lazy
chunk, unknown locales warn, and the README explains the divergence.
- Dev workflow: `rollup -c -w` with serve and livereload, dotenv wired
so the documented `.env` step actually loads. `npm run build` runs
offline; a separate `translate` script regenerates the translation files
with a production key.

## Testing

- typecheck, oxlint, offline build, frozen-lockfile install: all pass on
the current head. Injected `_hash` values match the fixture keys;
verified in a real browser across en, fr, ja including a `<Var>`
mid-sentence.
- The dotenv wiring was proven dynamically: a throwaway `.env` value
inlines into the bundle and disappears on a clean rebuild.
- The internal `tests/apps/webpack-react` build passes on this branch
(see the postcss note below).
- CI lints examples but does not build them.

## Notes

- One non-example change, disclosed plainly: `postcss` is pinned in the
workspace root devDependencies so every webpack consumer resolves a
single instance. Without it, this example's postcss plugin splits
webpack's peer context and breaks the `tests/apps/webpack-react` build.
The same line ships in #1944; whichever merges first carries it and the
other becomes a no-op. If there's a preferred way to manage the peer
graph, happy to redo this part.
- Honest limitation, documented in the README: on-the-fly dev
translation fetching does not activate under plain Rollup, because
nothing supplies gt-react an env signal in the browser. The committed
files are what renders. A dev define injected by the compiler's rollup
plugin would fix this generally; that would be a library change.
- Lockfile note: installing this example dedupes gt-sanity's build
toolchain from rollup 4.60.3 to 4.62.2 (semver-minor, reproducible,
frozen installs pass). The gt-sanity build failure visible locally is
pre-existing on clean main and unrelated.
- No changeset (example only).

<!-- greptile_comment -->

<h3>Greptile Summary</h3>

This PR adds a new `examples/rollup-spa` example: a React 19 SPA bundled
with pure Rollup and wired with `gt-react` for build-time i18n via the
GT compiler plugin. It is the Rollup counterpart to the concurrent
Vite/Webpack/RSBuild SPA PRs.

- **Build pipeline** (`rollup.config.mjs`): GT compiler runs first,
followed by `replace`, `nodeResolve`, `commonjs`, `json`, `typescript`,
`postcss` (extraction), and a small custom `htmlPlugin`. The CSS extract
path is shared via `CSS_ASSET_PATH` to prevent drift between the PostCSS
target and the injected `<link>` href.
- **Translation loading** (`src/loadTranslations.ts`): Uses an explicit
per-locale static import map instead of a template-literal dynamic
import, because Rollup cannot statically analyze a fully dynamic path;
each entry produces its own lazy chunk.
- **Non-example change** (`package.json` root): `postcss` is pinned as a
workspace-root devDependency to ensure a single peer instance across
Webpack consumers; an identical change ships in the sibling #1944 PR.

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

This is a well-isolated example addition; no production library code is
touched and the one non-example change (pinning postcss at the workspace
root) is a safe deduplication.

All changes are scoped to a new example directory plus two one-line
updates to workspace config files. The Rollup pipeline is correct, the
CSS path constant properly guards the extract/inject pair, and the
translation loader map matches the locales declared in gt.config.json.

No files require special attention; the pnpm-lock.yaml churn is expected
from the rollup deduplication described in the PR notes.
</details>

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

| Filename | Overview |
|----------|----------|
| examples/rollup-spa/rollup.config.mjs | Rollup build config with
correct plugin order, shared CSS_ASSET_PATH constant, and guarded
isWatch dev-server conditional; NODE_ENV defaults to 'production' in
watch mode (no explicit dev override) |
| examples/rollup-spa/src/loadTranslations.ts | Explicit static-import
locale map that lets Rollup code-split per-locale chunks; returns {}
with a console.warn for unknown locales; 'en' (default locale) is
correctly absent since the SPA runtime uses source strings for it |
| examples/rollup-spa/src/index.ts | Entry module using top-level await
to sequence initializeGTSPA before React mounts; gt-react/macros import
attaches the global t macro; correct ESM pattern for this setup |
| examples/rollup-spa/src/App.tsx | Demonstrates T component, Var
mid-sentence, and module-level t macro; locale switch triggers full page
reload as expected for gt-react SPA mode |
| examples/rollup-spa/vercel.json | Vercel deployment config using turbo
build with --env-mode=loose, SPA fallback rewrite to index.html;
consistent with other example deployments in the monorepo |
| .oxlintrc.json | Adds t as a readonly global to prevent
undefined-variable lint errors from the module-level t macro used by the
SPA examples |
| .changeset/config.json | Adds the five new SPA example package names
to the changeset ignore list; correct for example-only packages |

</details>

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

```mermaid
sequenceDiagram
    participant Browser
    participant index.ts as src/index.ts
    participant GTRuntime as initializeGTSPA (gt-react)
    participant loadTranslations as loadTranslations.ts
    participant JSON as _gt/<locale>.json chunk

    Browser->>index.ts: load ESM bundle
    index.ts->>GTRuntime: "await initializeGTSPA({ projectId, loadTranslations })"
    GTRuntime->>loadTranslations: loadTranslations(currentLocale)
    alt locale found in map (zh/fr/es/ja)
        loadTranslations->>JSON: "dynamic import('./_gt/<locale>.json')"
        JSON-->>loadTranslations: translations object
        loadTranslations-->>GTRuntime: translations
    else locale not in map
        loadTranslations-->>GTRuntime: "{} (+ console.warn)"
    end
    GTRuntime-->>index.ts: runtime ready
    index.ts->>index.ts: await import('./main')
    index.ts->>Browser: React tree mounts with translations
```
</details>

<sub>Reviews (7): Last reviewed commit: ["fix(examples): use the global
t macro
in..."](7b6105c)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=45751569)</sub>

<!-- /greptile_comment -->
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