Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/qwik (rebased to split PoC from qwik template install) #2101

Closed
wants to merge 10 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions examples/qwik-ts/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
**/*.log
**/.DS_Store
*.
.vscode/settings.json
.history
.yarn
bazel-*
bazel-bin
bazel-out
bazel-qwik
bazel-testlogs
dist
dist-dev
lib
lib-types
etc
external
node_modules
temp
tsc-out
tsdoc-metadata.json
target
output
rollup.config.js
build
.cache
.vscode
.rollup.cache
dist
tsconfig.tsbuildinfo
vite.config.ts
*.spec.tsx
*.spec.ts
.netlify
pnpm-lock.yaml
package-lock.json
yarn.lock
server
42 changes: 42 additions & 0 deletions examples/qwik-ts/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:qwik/recommended",
],
parser: "@typescript-eslint/parser",
parserOptions: {
tsconfigRootDir: __dirname,
project: ["./tsconfig.json"],
ecmaVersion: 2021,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
},
plugins: ["@typescript-eslint"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/ban-ts-comment": "off",
"prefer-spread": "off",
"no-case-declarations": "off",
"no-console": "off",
"@typescript-eslint/no-unused-vars": ["error"],
"@typescript-eslint/consistent-type-imports": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn",
},
};
41 changes: 41 additions & 0 deletions examples/qwik-ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Build
/dist
/lib
/lib-types
/server

# Development
node_modules
*.local

# Cache
.cache
.mf
.rollup.cache
tsconfig.tsbuildinfo

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# Editor
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets

.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

# Yarn
.yarn/*
!.yarn/releases
37 changes: 37 additions & 0 deletions examples/qwik-ts/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
**/*.log
**/.DS_Store
*.
.vscode/settings.json
.history
.yarn
bazel-*
bazel-bin
bazel-out
bazel-qwik
bazel-testlogs
dist
dist-dev
lib
lib-types
etc
external
node_modules
temp
tsc-out
tsdoc-metadata.json
target
output
rollup.config.js
build
.cache
.vscode
.rollup.cache
tsconfig.tsbuildinfo
vite.config.ts
*.spec.tsx
*.spec.ts
.netlify
pnpm-lock.yaml
package-lock.json
yarn.lock
server
24 changes: 24 additions & 0 deletions examples/qwik-ts/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Chrome",
"request": "launch",
"type": "chrome",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}"
},
{
"type": "node",
"name": "dev.debug",
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}/node_modules/vite/bin/vite.js",
"args": ["--mode", "ssr", "--force"]
}
]
}
36 changes: 36 additions & 0 deletions examples/qwik-ts/.vscode/qwik-city.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"onRequest": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qonRequest",
"description": "onRequest function for a route index",
"body": [
"export const onRequest: RequestHandler = (request) => {",
" $0",
"};",
],
},
"loader$": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qloader$",
"description": "loader$()",
"body": ["export const $1 = routeLoader$(() => {", " $0", "});"],
},
"action$": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qaction$",
"description": "action$()",
"body": ["export const $1 = routeAction$((data) => {", " $0", "});"],
},
"Full Page": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qpage",
"description": "Simple page component",
"body": [
"import { component$ } from '@builder.io/qwik';",
"",
"export default component$(() => {",
" $0",
"});",
],
},
}
78 changes: 78 additions & 0 deletions examples/qwik-ts/.vscode/qwik.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"Qwik component (simple)": {
"scope": "javascriptreact,typescriptreact",
"prefix": "qcomponent$",
"description": "Simple Qwik component",
"body": [
"export const ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}} = component$(() => {",
" return <${2:div}>$4</$2>",
"});",
],
},
"Qwik component (props)": {
"scope": "typescriptreact",
"prefix": "qcomponent$ + props",
"description": "Qwik component w/ props",
"body": [
"export interface ${1:${TM_FILENAME_BASE/(.*)/${1:/pascalcase}/}}Props {",
" $2",
"}",
"",
"export const $1 = component$<$1Props>((props) => {",
" const ${2:count} = useSignal(0);",
" return (",
" <${3:div} on${4:Click}$={(ev) => {$5}}>",
" $6",
" </${3}>",
" );",
"});",
],
},
"Qwik signal": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseSignal",
"description": "useSignal() declaration",
"body": ["const ${1:foo} = useSignal($2);", "$0"],
},
"Qwik store": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseStore",
"description": "useStore() declaration",
"body": ["const ${1:state} = useStore({", " $2", "});", "$0"],
},
"$ hook": {
"scope": "javascriptreact,typescriptreact",
"prefix": "q$",
"description": "$() function hook",
"body": ["$(() => {", " $0", "});", ""],
},
"useVisibleTask": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseVisibleTask",
"description": "useVisibleTask$() function hook",
"body": ["useVisibleTask$(({ track }) => {", " $0", "});", ""],
},
"useTask": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseTask$",
"description": "useTask$() function hook",
"body": [
"useTask$(({ track }) => {",
" track(() => $1);",
" $0",
"});",
"",
],
},
"useResource": {
"scope": "javascriptreact,typescriptreact",
"prefix": "quseResource$",
"description": "useResource$() declaration",
"body": [
"const $1 = useResource$(({ track, cleanup }) => {",
" $0",
"});",
"",
],
},
}
65 changes: 65 additions & 0 deletions examples/qwik-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Qwik City App ⚡️

- [Qwik Docs](https://qwik.dev/)
- [Discord](https://qwik.dev/chat)
- [Qwik GitHub](https://github.com/QwikDev/qwik)
- [@QwikDev](https://twitter.com/QwikDev)
- [Vite](https://vitejs.dev/)

---

## Project Structure

This project is using Qwik with [QwikCity](https://qwik.dev/qwikcity/overview/). QwikCity is just an extra set of tools on top of Qwik to make it easier to build a full site, including directory-based routing, layouts, and more.

Inside your project, you'll see the following directory structure:

```
├── public/
│ └── ...
└── src/
├── components/
│ └── ...
└── routes/
└── ...
```

- `src/routes`: Provides the directory-based routing, which can include a hierarchy of `layout.tsx` layout files, and an `index.tsx` file as the page. Additionally, `index.ts` files are endpoints. Please see the [routing docs](https://qwik.dev/qwikcity/routing/overview/) for more info.

- `src/components`: Recommended directory for components.

- `public`: Any static assets, like images, can be placed in the public directory. Please see the [Vite public directory](https://vitejs.dev/guide/assets.html#the-public-directory) for more info.

## Add Integrations and deployment

Use the `pnpm qwik add` command to add additional integrations. Some examples of integrations includes: Cloudflare, Netlify or Express Server, and the [Static Site Generator (SSG)](https://qwik.dev/qwikcity/guides/static-site-generation/).

```shell
pnpm qwik add # or `pnpm qwik add`
```

## Development

Development mode uses [Vite's development server](https://vitejs.dev/). The `dev` command will server-side render (SSR) the output during development.

```shell
npm start # or `pnpm start`
```

> Note: during dev mode, Vite may request a significant number of `.js` files. This does not represent a Qwik production build.
## Preview

The preview command will create a production build of the client modules, a production build of `src/entry.preview.tsx`, and run a local server. The preview server is only for convenience to preview a production build locally and should not be used as a production server.

```shell
pnpm preview # or `pnpm preview`
```

## Production

The production build will generate client and server modules by running both client and server build commands. The build command will use Typescript to run a type check on the source code.

```shell
pnpm build # or `pnpm build`
```
6,438 changes: 6,438 additions & 0 deletions examples/qwik-ts/package-lock.json

Large diffs are not rendered by default.

123 changes: 123 additions & 0 deletions examples/qwik-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"name": "my-qwik-empty-starter",
"description": "Blank project with routing included",
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"engines-annotation": "Mostly required by sharp which needs a Node-API v9 compatible runtime",
"private": true,
"trustedDependencies": [
"sharp"
],
"trustedDependencies-annotation": "Needed for bun to allow running install scripts",
"type": "module",
"scripts": {
"build": "qwik build",
"build.client": "vite build",
"build.preview": "vite build --ssr src/entry.preview.tsx",
"build.types": "tsc --incremental --noEmit",
"deploy": "echo 'Run \"npm run qwik add\" to install a server adapter'",
"dev": "vite --mode ssr",
"dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force",
"fmt": "prettier --write .",
"fmt.check": "prettier --check .",
"lint": "eslint \"src/**/*.ts*\"",
"preview": "qwik build preview && vite preview --open",
"start": "vite --open --mode ssr",
"qwik": "qwik"
},
"dependencies": {
"@internationalized/date": "3.5.5",
"@types/textarea-caret": "3.0.3",
"@zag-js/accordion": "workspace:*",
"@zag-js/anatomy": "workspace:*",
"@zag-js/anatomy-icons": "workspace:*",
"@zag-js/aria-hidden": "workspace:*",
"@zag-js/auto-resize": "workspace:*",
"@zag-js/avatar": "workspace:*",
"@zag-js/carousel": "workspace:*",
"@zag-js/checkbox": "workspace:*",
"@zag-js/clipboard": "workspace:*",
"@zag-js/collapsible": "workspace:*",
"@zag-js/collection": "workspace:*",
"@zag-js/color-picker": "workspace:*",
"@zag-js/color-utils": "workspace:*",
"@zag-js/combobox": "workspace:*",
"@zag-js/core": "workspace:*",
"@zag-js/date-picker": "workspace:*",
"@zag-js/date-utils": "workspace:*",
"@zag-js/dialog": "workspace:*",
"@zag-js/dismissable": "workspace:*",
"@zag-js/docs": "workspace:*",
"@zag-js/dom-event": "workspace:*",
"@zag-js/dom-query": "workspace:*",
"@zag-js/editable": "workspace:*",
"@zag-js/element-rect": "workspace:*",
"@zag-js/element-size": "workspace:*",
"@zag-js/file-upload": "workspace:*",
"@zag-js/file-utils": "workspace:*",
"@zag-js/floating-panel": "workspace:*",
"@zag-js/focus-trap": "workspace:*",
"@zag-js/focus-visible": "workspace:*",
"@zag-js/form-utils": "workspace:*",
"@zag-js/highlight-word": "workspace:*",
"@zag-js/hover-card": "workspace:*",
"@zag-js/i18n-utils": "workspace:*",
"@zag-js/interact-outside": "workspace:*",
"@zag-js/live-region": "workspace:*",
"@zag-js/menu": "workspace:*",
"@zag-js/number-input": "workspace:*",
"@zag-js/number-utils": "workspace:*",
"@zag-js/numeric-range": "workspace:*",
"@zag-js/pagination": "workspace:*",
"@zag-js/pin-input": "workspace:*",
"@zag-js/popover": "workspace:*",
"@zag-js/popper": "workspace:*",
"@zag-js/presence": "workspace:*",
"@zag-js/progress": "workspace:*",
"@zag-js/qr-code": "workspace:*",
"@zag-js/radio-group": "workspace:*",
"@zag-js/rating-group": "workspace:*",
"@zag-js/rect-utils": "workspace:*",
"@zag-js/remove-scroll": "workspace:*",
"@zag-js/select": "workspace:*",
"@zag-js/shared": "workspace:*",
"@zag-js/signature-pad": "workspace:*",
"@zag-js/slider": "workspace:*",
"@zag-js/splitter": "workspace:*",
"@zag-js/steps": "workspace:*",
"@zag-js/store": "workspace:*",
"@zag-js/stringify-state": "workspace:*",
"@zag-js/switch": "workspace:*",
"@zag-js/tabs": "workspace:*",
"@zag-js/tags-input": "workspace:*",
"@zag-js/text-selection": "workspace:*",
"@zag-js/time-picker": "workspace:*",
"@zag-js/timer": "workspace:*",
"@zag-js/toast": "workspace:*",
"@zag-js/toggle-group": "workspace:*",
"@zag-js/tooltip": "workspace:*",
"@zag-js/tour": "workspace:*",
"@zag-js/tree-view": "workspace:*",
"@zag-js/types": "workspace:*",
"@zag-js/utils": "workspace:*",
"form-serialize": "0.7.2",
"match-sorter": "6.3.4"
},
"devDependencies": {
"@builder.io/qwik": "^1.8.0",
"@builder.io/qwik-city": "^1.8.0",
"@types/eslint": "8.56.10",
"@types/node": "20.14.11",
"@typescript-eslint/eslint-plugin": "7.16.1",
"@typescript-eslint/parser": "7.16.1",
"lucide-qwik": "1.0.0",
"eslint": "8.57.0",
"eslint-plugin-qwik": "^1.8.0",
"prettier": "3.3.3",
"typescript": "5.4.5",
"undici": "*",
"vite": "5.3.5",
"vite-tsconfig-paths": "^4.2.1"
}
}
1 change: 1 addition & 0 deletions examples/qwik-ts/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions examples/qwik-ts/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/web-manifest-combined.json",
"name": "qwik-project-name",
"short_name": "Welcome to Qwik",
"start_url": ".",
"display": "standalone",
"background_color": "#fff",
"description": "A Qwik project app."
}
Empty file.
4 changes: 4 additions & 0 deletions examples/qwik-ts/qwik.env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This file can be used to add references for global types like `vite/client`.

// Add global `vite/client` types. For more info, see: https://vitejs.dev/guide/features#client-types
/// <reference types="vite/client" />
44 changes: 44 additions & 0 deletions examples/qwik-ts/src/components/router-head/router-head.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { component$ } from "@builder.io/qwik"
import { useDocumentHead, useLocation } from "@builder.io/qwik-city"

/**
* The RouterHead component is placed inside of the document `<head>` element.
*/
export const RouterHead = component$(() => {
const head = useDocumentHead()
const loc = useLocation()

return (
<>
<title>{head.title}</title>

<link rel="canonical" href={loc.url.href} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />

{head.meta.map((m) => (
<meta key={m.key} {...m} />
))}

{head.links.map((l) => (
<link key={l.key} {...l} />
))}

{head.styles.map((s) => (
<style
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML ? {} : { dangerouslySetInnerHTML: s.style })}
/>
))}

{head.scripts.map((s) => (
<script
key={s.key}
{...s.props}
{...(s.props?.dangerouslySetInnerHTML ? {} : { dangerouslySetInnerHTML: s.script })}
/>
))}
</>
)
})
17 changes: 17 additions & 0 deletions examples/qwik-ts/src/entry.dev.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* WHAT IS THIS FILE?
*
* Development entry point using only client-side modules:
* - Do not use this mode in production!
* - No SSR
* - No portion of the application is pre-rendered on the server.
* - All of the application is running eagerly in the browser.
* - More code is transferred to the browser than in SSR mode.
* - Optimizer/Serialization/Deserialization code is not exercised!
*/
import { render, type RenderOptions } from "@builder.io/qwik"
import Root from "./root"

export default function (opts: RenderOptions) {
return render(document, <Root />, opts)
}
21 changes: 21 additions & 0 deletions examples/qwik-ts/src/entry.preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* WHAT IS THIS FILE?
*
* It's the bundle entry point for `npm run preview`.
* That is, serving your app built in production mode.
*
* Feel free to modify this file, but don't remove it!
*
* Learn more about Vite's preview command:
* - https://vitejs.dev/config/preview-options.html#preview-options
*
*/
import { createQwikCity } from "@builder.io/qwik-city/middleware/node"
import qwikCityPlan from "@qwik-city-plan"
// make sure qwikCityPlan is imported before entry
import render from "./entry.ssr"

/**
* The default export is the QwikCity adapter used by Vite preview.
*/
export default createQwikCity({ render, qwikCityPlan })
30 changes: 30 additions & 0 deletions examples/qwik-ts/src/entry.ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* WHAT IS THIS FILE?
*
* SSR entry point, in all cases the application is rendered outside the browser, this
* entry point will be the common one.
*
* - Server (express, cloudflare...)
* - npm run start
* - npm run preview
* - npm run build
*
*/
import { renderToStream, type RenderToStreamOptions } from "@builder.io/qwik/server"
import { manifest } from "@qwik-client-manifest"
import Root from "./root"

export default function (opts: RenderToStreamOptions) {
return renderToStream(<Root />, {
manifest,
...opts,
// Use container attributes to set attributes on the html tag.
containerAttributes: {
lang: "en-us",
...opts.containerAttributes,
},
serverData: {
...opts.serverData,
},
})
}
Empty file added examples/qwik-ts/src/global.css
Empty file.
46 changes: 46 additions & 0 deletions examples/qwik-ts/src/hooks/normalize-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { CSSProperties, HTMLAttributes, IntrinsicElements } from "@builder.io/qwik"
import { createNormalizer } from "@zag-js/types"

export type PropTypes = IntrinsicElements & {
element: HTMLAttributes<any>
style: CSSProperties
}

const propMap: Record<string, string> = {
htmlFor: "for",
className: "class",
onDoubleClick: "onDblClick$",
onChange: "onInput$",
onFocus: "onFocus$",
onBlur: "onBlur$",
defaultValue: "value",
defaultChecked: "checked",
}

function toQwikProp(prop: string) {
if (prop in propMap) return propMap[prop]

if (prop.startsWith("on")) {
return prop + "$"
}

return prop
}

function toQwikPropValue(key: string, value: Dict[string]) {
if (value === false) return

return value
}

type Dict = Record<string, any>

export const normalizeProps = createNormalizer<PropTypes>((props) => {
const normalized: Dict = {}

for (const key in props) {
normalized[toQwikProp(key)] = toQwikPropValue(key, props[key])
}

return normalized
})
21 changes: 21 additions & 0 deletions examples/qwik-ts/src/hooks/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { NoSerialize, QRL, Signal } from "@builder.io/qwik"
import type { Machine, StateMachine as S } from "@zag-js/core"

export interface UseMachineOptions<
TContext extends Record<string, any>,
TState extends S.StateSchema,
TEvent extends S.EventObject,
> {
qrl: () => Promise<NoSerialize<Machine<TContext, TState, TEvent>>>
initialState: NoSerialize<S.State<TContext, TState>>
}

export type UseMachineReturn<
TContext extends Record<string, any>,
TState extends S.StateSchema,
TEvent extends S.EventObject,
> = [
Signal<NoSerialize<S.State<TContext, TState>> | null>,
QRL<(event: TEvent | string) => void>,
Signal<NoSerialize<Machine<TContext, TState, TEvent>> | null>,
]
17 changes: 17 additions & 0 deletions examples/qwik-ts/src/hooks/use-actor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Machine, StateMachine as S } from "@zag-js/core"
import { useSnapshot } from "./use-snapshot"
import { $, NoSerialize, Signal } from "@builder.io/qwik"

export function useActor<
TContext extends Record<string, any>,
TState extends S.StateSchema,
TEvent extends S.EventObject = S.AnyEventObject,
>(service: Signal<NoSerialize<Machine<TContext, TState, TEvent>> | null>) {
const state = useSnapshot(service)

const send = $((event: TEvent | string) => {
service.value?.send(event)
})

return [state, send] as const
}
23 changes: 23 additions & 0 deletions examples/qwik-ts/src/hooks/use-machine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { $ } from "@builder.io/qwik"
import type { StateMachine as S } from "@zag-js/core"
import { useService } from "./use-service"
import { useSnapshot } from "./use-snapshot"
import { UseMachineOptions, UseMachineReturn } from "~/hooks/types"

export function useMachine<
TContext extends Record<string, any>,
TState extends S.StateSchema,
TEvent extends S.EventObject,
>(
props: UseMachineOptions<TContext, TState, TEvent>,
options?: S.HookOptions<TContext, TState, TEvent>,
): UseMachineReturn<TContext, TState, TEvent> {
const service = useService(props, options)
const state = useSnapshot(service, options, props.initialState)

const send = $((event: TEvent | string) => {
service.value?.send(event)
})

return [state, send, service]
}
38 changes: 38 additions & 0 deletions examples/qwik-ts/src/hooks/use-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { NoSerialize, noSerialize, useComputed$, useStore, useVisibleTask$ } from "@builder.io/qwik"
import type { Machine, StateMachine as S } from "@zag-js/core"
import { UseMachineOptions } from "~/hooks/types"

export function useService<
TContext extends Record<string, any>,
TState extends S.StateSchema,
TEvent extends S.EventObject,
>(props: UseMachineOptions<TContext, TState, TEvent>, options?: S.HookOptions<TContext, TState, TEvent>) {
const { qrl } = props
const { state: hydratedState, context } = options ?? {}

const store = useStore<{
service: NoSerialize<Machine<TContext, TState, TEvent>> | null
}>({
service: null,
})

useVisibleTask$(async ({ track, cleanup }) => {
// Load the service
const service = await qrl()

if (context) {
service!.setContext(context)
track(context)
}

service!.start(hydratedState)

store.service = noSerialize(service)

cleanup(() => {
service!.stop()
})
})

return useComputed$(() => store.service)
}
37 changes: 37 additions & 0 deletions examples/qwik-ts/src/hooks/use-snapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NoSerialize, noSerialize, Signal, useComputed$, useSignal, useVisibleTask$ } from "@builder.io/qwik"
import type { Machine, StateMachine as S } from "@zag-js/core"

export function useSnapshot<
TContext extends Record<string, any>,
TState extends S.StateSchema,
TEvent extends S.EventObject,
>(
serviceSignal: Signal<NoSerialize<Machine<TContext, TState, TEvent>> | null>,
options?: S.HookOptions<TContext, TState, TEvent>,
initialState?: NoSerialize<S.State<TContext, TState, S.AnyEventObject>>,
) {
const { actions } = options ?? {}

const stateSignal = useSignal<NoSerialize<S.State<TContext, TState>> | null>(initialState)

useVisibleTask$(({ track, cleanup }) => {
track(() => serviceSignal.value)

const service = serviceSignal.value
if (!service) return

stateSignal.value = noSerialize(service.getState())

const unsubscribe = service.subscribe((state: any) => {
stateSignal.value = noSerialize(state)
})

service.setOptions({ actions })

cleanup(() => {
unsubscribe()
})
})

return useComputed$(() => stateSignal.value)
}
29 changes: 29 additions & 0 deletions examples/qwik-ts/src/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { component$ } from "@builder.io/qwik"
import { QwikCityProvider, RouterOutlet, ServiceWorkerRegister } from "@builder.io/qwik-city"
import { RouterHead } from "./components/router-head/router-head"
import { isDev } from "@builder.io/qwik/build"

import "./global.css"

export default component$(() => {
/**
* The root of a QwikCity site always start with the <QwikCityProvider> component,
* immediately followed by the document's <head> and <body>.
*
* Don't remove the `<head>` and `<body>` elements.
*/

return (
<QwikCityProvider>
<head>
<meta charset="utf-8" />
{!isDev && <link rel="manifest" href={`${import.meta.env.BASE_URL}manifest.json`} />}
<RouterHead />
</head>
<body lang="en">
<RouterOutlet />
{!isDev && <ServiceWorkerRegister />}
</body>
</QwikCityProvider>
)
})
89 changes: 89 additions & 0 deletions examples/qwik-ts/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { $, component$, noSerialize, useStore } from "@builder.io/qwik"
import type { DocumentHead } from "@builder.io/qwik-city"
import { createMachine } from "@zag-js/core"
import { useMachine } from "~/hooks/use-machine"
import type { NormalizeProps } from "@zag-js/types"
import { normalizeProps } from "~/hooks/normalize-props"

const machine = (props: { count?: number; onCount?: (count: number) => void }) => {
return createMachine({
context: { count: 0, ...props },
initial: "idle",
states: {
idle: {
on: {
INCREMENT: {
actions(ctx) {
ctx.count += 1
ctx.onCount?.(ctx.count)
},
},
},
},
},
})
}

function connect(state: any, send: any, normalize: NormalizeProps<any>) {
return {
count: state.context.count,

getButtonProps() {
return normalize.element({
"data-count": state.context.count,
disabled: state.context.count >= 15,
onClick: $(() => {
send("INCREMENT")
}),
})
},
}
}

export default component$(() => {
const context = useStore({
count: 10,
})

const [state, send] = useMachine(
{
qrl: $(() =>
noSerialize(
machine({
onCount(count) {
context.count = count
},
}),
),
),
initialState: noSerialize(machine({ count: 10 }).getState()),
},
{
context,
},
)

const api = connect(state.value, send, normalizeProps)

return (
<>
<h1>Hi 👋</h1>
<div>
Count is: {api.count}
<br />
<button onClick$={() => (context.count += 1)}>Controlled Increment</button>
<button {...api.getButtonProps()}>Increment</button>
</div>
</>
)
})

export const head: DocumentHead = {
title: "Welcome to Qwik",
meta: [
{
name: "description",
content: "Qwik site description",
},
],
}
17 changes: 17 additions & 0 deletions examples/qwik-ts/src/routes/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { component$, Slot } from "@builder.io/qwik"
import type { RequestHandler } from "@builder.io/qwik-city"

export const onGet: RequestHandler = async ({ cacheControl }) => {
// Control caching for this request for best performance and to reduce hosting costs:
// https://qwik.dev/docs/caching/
cacheControl({
// Always serve a cached response by default, up to a week stale
staleWhileRevalidate: 60 * 60 * 24 * 7,
// Max once every 5 seconds, revalidate on the server to get a fresh version of this page
maxAge: 5,
})
}

export default component$(() => {
return <Slot />
})
18 changes: 18 additions & 0 deletions examples/qwik-ts/src/routes/service-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* WHAT IS THIS FILE?
*
* The service-worker.ts file is used to have state of the art prefetching.
* https://qwik.dev/qwikcity/prefetching/overview/
*
* Qwik uses a service worker to speed up your site and reduce latency, ie, not used in the traditional way of offline.
* You can also use this file to add more functionality that runs in the service worker.
*/
import { setupServiceWorker } from "@builder.io/qwik-city/service-worker"

setupServiceWorker()

addEventListener("install", () => self.skipWaiting())

addEventListener("activate", () => self.clients.claim())

declare const self: ServiceWorkerGlobalScope
25 changes: 25 additions & 0 deletions examples/qwik-ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"allowJs": true,
"target": "ES2017",
"module": "ES2022",
"lib": ["es2022", "DOM", "WebWorker", "DOM.Iterable"],
"jsx": "react-jsx",
"jsxImportSource": "@builder.io/qwik",
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"moduleResolution": "Bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"incremental": true,
"isolatedModules": true,
"outDir": "tmp",
"noEmit": true,
"paths": {
"~/*": ["./src/*"]
}
},
"files": ["./.eslintrc.cjs"],
"include": ["src", "./*.d.ts", "./*.config.ts"]
}
99 changes: 99 additions & 0 deletions examples/qwik-ts/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* This is the base config for vite.
* When building, the adapter config is used which loads this file and extends it.
*/
import { defineConfig, type UserConfig } from "vite"
import { qwikVite } from "@builder.io/qwik/optimizer"
import { qwikCity } from "@builder.io/qwik-city/vite"
import tsconfigPaths from "vite-tsconfig-paths"
import pkg from "./package.json"

type PkgDep = Record<string, string>
const { dependencies = {}, devDependencies = {} } = pkg as any as {
dependencies: PkgDep
devDependencies: PkgDep
[key: string]: unknown
}
errorOnDuplicatesPkgDeps(devDependencies, dependencies)

/**
* Note that Vite normally starts from `index.html` but the qwikCity plugin makes start at `src/entry.ssr.tsx` instead.
*/
export default defineConfig(({ command, mode }): UserConfig => {
return {
plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
// This tells Vite which dependencies to pre-build in dev mode.
optimizeDeps: {
// Put problematic deps that break bundling here, mostly those with binaries.
// For example ['better-sqlite3'] if you use that in server functions.
exclude: [],
},

/**
* This is an advanced setting. It improves the bundling of your server code. To use it, make sure you understand when your consumed packages are dependencies or dev dependencies. (otherwise things will break in production)
*/
// ssr:
// command === "build" && mode === "production"
// ? {
// // All dev dependencies should be bundled in the server build
// noExternal: Object.keys(devDependencies),
// // Anything marked as a dependency will not be bundled
// // These should only be production binary deps (including deps of deps), CLI deps, and their module graph
// // If a dep-of-dep needs to be external, add it here
// // For example, if something uses `bcrypt` but you don't have it as a dep, you can write
// // external: [...Object.keys(dependencies), 'bcrypt']
// external: Object.keys(dependencies),
// }
// : undefined,

server: {
headers: {
// Don't cache the server response in dev mode
"Cache-Control": "public, max-age=0",
},
},
preview: {
headers: {
// Do cache the server response in preview (non-adapter production build)
"Cache-Control": "public, max-age=600",
},
},
}
})

// *** utils ***

/**
* Function to identify duplicate dependencies and throw an error
* @param {Object} devDependencies - List of development dependencies
* @param {Object} dependencies - List of production dependencies
*/
function errorOnDuplicatesPkgDeps(devDependencies: PkgDep, dependencies: PkgDep) {
let msg = ""
// Create an array 'duplicateDeps' by filtering devDependencies.
// If a dependency also exists in dependencies, it is considered a duplicate.
const duplicateDeps = Object.keys(devDependencies).filter((dep) => dependencies[dep])

// include any known qwik packages
const qwikPkg = Object.keys(dependencies).filter((value) => /qwik/i.test(value))

// any errors for missing "qwik-city-plan"
// [PLUGIN_ERROR]: Invalid module "@qwik-city-plan" is not a valid package
msg = `Move qwik packages ${qwikPkg.join(", ")} to devDependencies`

if (qwikPkg.length > 0) {
throw new Error(msg)
}

// Format the error message with the duplicates list.
// The `join` function is used to represent the elements of the 'duplicateDeps' array as a comma-separated string.
msg = `
Warning: The dependency "${duplicateDeps.join(", ")}" is listed in both "devDependencies" and "dependencies".
Please move the duplicated dependencies to "devDependencies" only and remove it from "dependencies"
`

// Throw an error with the constructed message.
if (duplicateDeps.length > 0) {
throw new Error(msg)
}
}
1,367 changes: 1,356 additions & 11 deletions pnpm-lock.yaml

Large diffs are not rendered by default.