diff --git a/.github/ISSUE_TEMPLATE/bug.yaml b/.github/ISSUE_TEMPLATE/bug.yaml index 75678124cb4..cb2cefd937e 100644 --- a/.github/ISSUE_TEMPLATE/bug.yaml +++ b/.github/ISSUE_TEMPLATE/bug.yaml @@ -53,7 +53,7 @@ body: id: system-info attributes: label: System Info - description: Output of `npx envinfo --system --npmPackages '{vite,undici,typescript,@builder.io/*}' --binaries --browsers` + description: Output of `npx envinfo --system --npmPackages '{vite,typescript,@builder.io/*}' --binaries --browsers` render: shell placeholder: System, Binaries, Browsers validations: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dca48949f91..4fd0f15d081 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,9 +51,7 @@ jobs: build-qwik: ${{ steps.cache-qwik.outputs.cache-hit != 'true' }} build-rust: ${{ steps.cache-rust.outputs.cache-hit != 'true' }} build-others: ${{ steps.cache-others.outputs.cache-hit != 'true' }} - # TEMP v2: disable docs build until we have v2 deps building (qwik-ui, qwik-sdk) - # build-docs: ${{ steps.cache-docs.outputs.cache-hit != 'true' }} - build-docs: false + build-docs: ${{ steps.cache-docs.outputs.cache-hit != 'true' }} # TEMP v2: disable insights build # build-insights: ${{ steps.cache-insights.outputs.cache-hit != 'true' }} build-insights: false @@ -510,6 +508,15 @@ jobs: - name: Build Qwik Docs run: pnpm run build.packages.docs + - name: Cloudflare Pages Deployment + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: 'qwik-docs' + directory: packages/docs/dist + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + ############ RELEASE ############ release: name: Release diff --git a/contributing/TRIAGE.md b/contributing/TRIAGE.md index 2f2fe4af6d8..c91ded29619 100644 --- a/contributing/TRIAGE.md +++ b/contributing/TRIAGE.md @@ -38,7 +38,7 @@ flowchart TD maj --YES--> P4[P4: urgent] maj --NO--> P3[P3: important] unusable --NO--> workarounds{Are there\nworkarounds for\nthe bug?} - workarounds --NO--> P2[P2: minor bug] + workarounds --NO--> P2[P2: minor] workarounds --YES--> P1[P1: nice to have / fix] ``` @@ -74,7 +74,7 @@ flowchart TD discussion --YES--> close3[Tag with\n 'STATUS-2: requires discussion'\nand 'WAITING FOR: team'\nor 'WAITING FOR: user'] discussion --NO--> implement{Should it be\nimplemented by core?} implement --NO--> community{Should it be implemented\nby the community?} - community --YES--> incubate[Close and tag with\n'STATUS-3: incubation'] + community --YES--> incubate[Close and tag with either\n'STATUS-3: incubation'\nor 'STATUS-2: waiting for community PR'\nand 'COMMUNITY: PR is welcomed'] community --NO--> wontfix[Close and tag with\n'STATUS-3: won't be worked on'] implement --YES--> doimplement["1. Tag with 'STATUS-2: team is working on this'\n2. Add related feature label if\napplicable (e.g. 'COMP: runtime')\n3. Add version \nlabels (e.g. 'VERSION: upcoming major')"] ``` diff --git a/package.json b/package.json index 5c91c74317c..b0af945cbd0 100644 --- a/package.json +++ b/package.json @@ -29,15 +29,6 @@ ], "pinVersion": "1.40.0" } - ], - "semverGroups": [ - { - "label": "Undici should always be * until we remove it", - "range": "*", - "dependencies": [ - "undici" - ] - } ] } }, @@ -68,22 +59,22 @@ "@clack/prompts": "^0.7.0", "@eslint/eslintrc": "^3.0.2", "@mdx-js/mdx": "^3.0.1", - "@microsoft/api-documenter": "^7.24.2", - "@microsoft/api-extractor": "^7.43.1", + "@microsoft/api-documenter": "7.24.2", + "@microsoft/api-extractor": "7.43.1", "@napi-rs/cli": "^2.18.2", "@napi-rs/triples": "^1.2.0", "@node-rs/helper": "^1.6.0", "@octokit/action": "6.1.0", "@playwright/test": "1.40.0", "@types/brotli": "^1.3.4", - "@types/bun": "^1.1.1", + "@types/bun": "^1.1.3", "@types/cross-spawn": "^6.0.6", "@types/eslint": "^8.56.10", "@types/express": "^4.17.21", - "@types/node": "^20.12.8", + "@types/node": "^20.14.1", "@types/path-browserify": "^1.0.2", "@types/prompts": "^2.4.9", - "@types/react": "^18.3.1", + "@types/react": "^18.3.3", "@types/semver": "^7.5.8", "@types/which-pm-runs": "^1.0.2", "@typescript-eslint/eslint-plugin": "^7.8.0", @@ -123,7 +114,6 @@ "terser": "^5.31.0", "tsm": "^2.3.0", "typescript": "5.4.5", - "undici": "*", "vfile": "^6.0.1", "vite": "^5.2.11", "vite-imagetools": "^6.2.9", @@ -132,10 +122,10 @@ "vitest": "^1.6.0", "watchlist": "0.3.1", "which-pm-runs": "1.1.0", - "zod": "^3.23.6" + "zod": "^3.23.8" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", "npm": "please-use-pnpm", "yarn": "please-use-pnpm", "pnpm": ">=9.0.5" @@ -145,6 +135,7 @@ "overrides": { "typescript": "5.4.5", "vfile": "^6.0.1", + "sharp": ">=0.33", "@supabase/realtime-js": "2.8.4" }, "patchedDependencies": { @@ -200,10 +191,10 @@ "start": "concurrently \"npm:build.watch\" \"npm:tsc.watch\" -n build,tsc -c green,cyan", "test": "pnpm build.full && pnpm test.unit && pnpm test.e2e", "test.e2e": "pnpm test.e2e.chromium && pnpm test.e2e.webkit", - "test.e2e.cli": "tsm scripts/e2e-cli.ts", "test.e2e.chromium": "playwright test starters --browser=chromium --config starters/playwright.config.ts", "test.e2e.chromium.debug": "PWDEBUG=1 playwright test starters --browser=chromium --config starters/playwright.config.ts", "test.e2e.city": "playwright test starters/e2e/qwikcity --browser=chromium --config starters/playwright.config.ts", + "test.e2e.cli": "tsm scripts/e2e-cli.ts", "test.e2e.firefox": "playwright test starters --browser=firefox --config starters/playwright.config.ts", "test.e2e.webkit": "playwright test starters --browser=webkit --config starters/playwright.config.ts", "test.rust": "make test", diff --git a/packages/create-qwik/src/helpers/jokes.json b/packages/create-qwik/src/helpers/jokes.json index 1fbb6468efb..08eb71b78b7 100644 --- a/packages/create-qwik/src/helpers/jokes.json +++ b/packages/create-qwik/src/helpers/jokes.json @@ -162,5 +162,8 @@ "Knock knock. \n Who's there? \n Opportunity.", "That is impossible. Opportunity doesn’t come knocking twice!" ], - ["Knock knock. \n Who's there? \n Hatch. \n Hatch who?", "Bless you!"] + ["Knock knock. \n Who's there? \n Hatch. \n Hatch who?", "Bless you!"], + ["Man said: I changed my Mind, Machine replied: I changed my CPU"], + ["Man said: I dreamed of God, Machine replied: I dreamed of Rust"], + ["Person: give me just a second, Programmer: give me just a 100 milliseconds"] ] diff --git a/packages/docs/check-qwik-build.ts b/packages/docs/check-qwik-build.ts index 223a7236924..ebcecad8981 100644 --- a/packages/docs/check-qwik-build.ts +++ b/packages/docs/check-qwik-build.ts @@ -3,8 +3,9 @@ import fs from 'fs'; import path from 'path'; import { spawnSync } from 'child_process'; +import { fileURLToPath } from 'url'; -const __dirname = path.dirname(new URL(import.meta.url).pathname); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const qwikPkgDir = path.join(__dirname, '..', 'qwik', 'dist'); if (!fs.existsSync(path.join(qwikPkgDir, 'core.d.ts'))) { diff --git a/packages/docs/contributors.ts b/packages/docs/contributors.ts index 0550f4b40d7..22877e9bcfa 100644 --- a/packages/docs/contributors.ts +++ b/packages/docs/contributors.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import { fetch } from 'undici'; import fs from 'node:fs'; import path from 'node:path'; import url from 'node:url'; diff --git a/packages/docs/package.json b/packages/docs/package.json index 631b908707a..fd750c2e5a2 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -12,20 +12,16 @@ "@builder.io/qwik-city": "workspace:^", "@builder.io/qwik-labs": "workspace:^", "@builder.io/qwik-react": "workspace:^", - "@builder.io/sdk-qwik": "^0.14.21", "@docsearch/css": "^3.5.2", "@emotion/react": "^11.11.4", "@emotion/styled": "^11.11.0", - "@modular-forms/qwik": "^0.23.1", "@mui/material": "^5.15.14", "@mui/system": "^5.15.14", "@mui/x-data-grid": "^6.19.6", "@supabase/supabase-js": "^2.39.8", "@types/prismjs": "^1.26.3", - "@types/react": "^18.3.1", + "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@unpic/core": "^0.0.42", - "@unpic/qwik": "^0.0.38", "algoliasearch": "4.16.0", "autoprefixer": "^10.4.19", "fflate": "^0.8.2", @@ -36,7 +32,7 @@ "prism-themes": "1.9.0", "prismjs": "1.29.0", "puppeteer": "^22.6.0", - "qwik-image": "^0.0.10", + "qwik-image": "0.0.14-alpha", "react": "18.3.1", "react-dom": "18.3.1", "rehype-pretty-code": "^0.11.0", @@ -47,14 +43,13 @@ "terser": "^5.31.0", "tsm": "^2.3.0", "typescript": "5.4.5", - "undici": "*", "valibot": "^0.29.0", "vite": "^5.2.11", "vite-plugin-inspect": "^0.8.4", "wrangler": "^3.53.1" }, "engines": { - "node": ">=18.11", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", "npm": "please-use-pnpm", "yarn": "please-use-pnpm", "pnpm": ">=8.6.12" diff --git a/packages/docs/public/showcases/ohayo-dev-design_com_.webp b/packages/docs/public/showcases/ohayo-dev-design_com_.webp new file mode 100644 index 00000000000..ebe82b96761 Binary files /dev/null and b/packages/docs/public/showcases/ohayo-dev-design_com_.webp differ diff --git a/packages/docs/scripts/pages.json b/packages/docs/scripts/pages.json index 419865885d2..4d173a951d7 100644 --- a/packages/docs/scripts/pages.json +++ b/packages/docs/scripts/pages.json @@ -4,6 +4,10 @@ "size": "large", "tags": "site,healthcare,services,webrtc,websocket" }, + { + "href": "https://ohayo-dev-design.com/", + "tags": "web, dev, agency" + }, { "href": "https://linkfang-portfolio.vercel.app/", "tags": "portfolio" diff --git a/packages/docs/scripts/showcase.js b/packages/docs/scripts/showcase.js index 74064e92901..9eaea2e4887 100644 --- a/packages/docs/scripts/showcase.js +++ b/packages/docs/scripts/showcase.js @@ -134,7 +134,6 @@ async function captureMultipleScreenshots() { } async function getPagespeedData(url) { - const { fetch } = await import('undici'); const requestURL = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent( url )}&key=AIzaSyApBC9gblaCzWrtEBgHnZkd_B37OF49BfM&category=PERFORMANCE&strategy=MOBILE`; diff --git a/packages/docs/src/components/builder-content/index.tsx b/packages/docs/src/components/builder-content/index.tsx index c915bf32a2d..fc40328d29f 100644 --- a/packages/docs/src/components/builder-content/index.tsx +++ b/packages/docs/src/components/builder-content/index.tsx @@ -1,6 +1,5 @@ import { component$, Resource, useResource$ } from '@builder.io/qwik'; import { useLocation } from '@builder.io/qwik-city'; -import { getBuilderSearchParams, fetchOneEntry, Content } from '@builder.io/sdk-qwik'; import { QWIK_MODEL } from '../../constants'; export default component$<{ @@ -17,52 +16,24 @@ export default component$<{ ? query.get(name) : (query as unknown as Record)[name]; - const render = queryGet('render'); const contentId = props.model === QWIK_MODEL ? queryGet('content') : undefined; - const isSDK = render === 'sdk'; cache('immutable'); - if (isSDK) { - return getCachedValue( - { - model: props.model!, - apiKey: props.apiKey!, - options: getBuilderSearchParams(query), - userAttributes: { - urlPath: location.url.pathname, - site: 'qwik.dev', - }, - ...(contentId && { - query: { - id: contentId, - }, - }), - }, - fetchOneEntry - ); - } else { - return getCachedValue( - { - apiKey: props.apiKey, - model: props.model, - urlPath: location.url.pathname, - contentId: contentId, - }, - getBuilderContent - ); - } + return getCachedValue( + { + apiKey: props.apiKey, + model: props.model, + urlPath: location.url.pathname, + contentId: contentId, + }, + getBuilderContent + ); }); return (
Loading...
} - onResolved={(content) => - content.html ? ( - - ) : ( - - ) - } + onResolved={(content) => } /> ); }); diff --git a/packages/docs/src/components/docsearch/result.tsx b/packages/docs/src/components/docsearch/result.tsx index 1e3908b9d2b..63f48a978c1 100644 --- a/packages/docs/src/components/docsearch/result.tsx +++ b/packages/docs/src/components/docsearch/result.tsx @@ -1,5 +1,5 @@ import { Slot, component$, useContext, useSignal, useStore, useTask$ } from '@builder.io/qwik'; -import { QwikGPT } from '../qwik-gpt'; +// import { QwikGPT } from '../qwik-gpt'; import { SearchContext } from './context'; import { AiResultOpenContext, type DocSearchState } from './doc-search'; import { Snippet } from './snippet'; @@ -134,7 +134,7 @@ export const AIButton = component$(({ state }: { state: DocSearchState }) => { } }} > -
+ {/*
)} -
+ */} )} diff --git a/packages/docs/src/components/docsearch/results-screen.tsx b/packages/docs/src/components/docsearch/results-screen.tsx index a2ea120f1ee..c6eb7e68f2f 100644 --- a/packages/docs/src/components/docsearch/results-screen.tsx +++ b/packages/docs/src/components/docsearch/results-screen.tsx @@ -21,7 +21,8 @@ export const ResultsScreen = component$((props: { state: DocSearchState }) => {
    {collection.items.map((item, index) => { return ( - + // TODO: the key should be {item.objectID}, but for now in v2 there is a bug + {item.__docsearch_parent && ( { enterKeyHint={props.state.activeItemId ? 'go' : 'search'} spellcheck={false} autoFocus={props.autoFocus} - placeholder="Search docs or ask a question" + placeholder="Search docs" type="search" ref={props.inputRef as any} onInput$={(event) => { diff --git a/packages/docs/src/components/footer/footer.tsx b/packages/docs/src/components/footer/footer.tsx index b9005fc73c4..056af96a596 100644 --- a/packages/docs/src/components/footer/footer.tsx +++ b/packages/docs/src/components/footer/footer.tsx @@ -1,11 +1,98 @@ import { component$ } from '@builder.io/qwik'; -import BuilderContentComp from '../../components/builder-content'; -import { BUILDER_FOOTER_MODEL, BUILDER_PUBLIC_API_KEY } from '../../constants'; +import { QwikLogo } from '~/components/svgs/qwik-logo'; +import { DiscordLogo } from '~/components/svgs/discord-logo'; +import { GithubLogo } from '~/components/svgs/github-logo'; +import { TwitterLogo } from '~/components/svgs/twitter-logo'; + +const baseUrl = 'https://qwik.dev'; +const linkColumns = [ + [ + { title: 'Docs', href: `${baseUrl}/docs/` }, + { title: 'Qwik City', href: `${baseUrl}/docs/qwikcity/` }, + { title: 'Ecosystem', href: `${baseUrl}/ecosystem/` }, + { title: 'Playground', href: `${baseUrl}/playground/` }, + ], + [ + { title: 'Integrations', href: `${baseUrl}/ecosystem/#integrations` }, + { title: 'Deployments', href: `${baseUrl}/ecosystem/#deployments` }, + { title: 'Media', href: `${baseUrl}/ecosystem/#videos` }, + { title: 'Showcase', href: `${baseUrl}/showcase/` }, + ], + [ + { title: 'Tutorial', href: `${baseUrl}/ecosystem/#courses` }, + { title: 'Presentations', href: `${baseUrl}/ecosystem/#presentations` }, + { title: 'Community', href: `${baseUrl}/ecosystem/#community` }, + ], +]; export const Footer = component$(() => { return ( -
    - +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +

    + Made with ❤️ by +

    +

    + The Qwik Team +

    +
    +
    +
    +

    MIT License © 2024

    + +
    +
    +
    ); }); + +export const FooterLinks = component$(() => { + return ( + <> + {linkColumns.map((column, colIndex) => ( +
    + {column.map((link, linkIndex) => ( + + {link.title} + + ))} +
    + ))} + + ); +}); + +export const FooterSocialLinks = component$(() => { + const socialLinks = [ + { href: 'https://qwik.dev/chat', title: 'Discord', Logo: DiscordLogo }, + { href: 'https://github.com/QwikDev/qwik', title: 'GitHub', Logo: GithubLogo }, + { href: 'https://twitter.com/QwikDev', title: 'Twitter', Logo: TwitterLogo }, + ]; + + return ( +
    + {socialLinks.map(({ title, href, Logo }) => ( +
  • + + + + + +
  • + ))} +
    + ); +}); diff --git a/packages/docs/src/components/header/header.tsx b/packages/docs/src/components/header/header.tsx index 9075c1dca07..a9aa3a20816 100644 --- a/packages/docs/src/components/header/header.tsx +++ b/packages/docs/src/components/header/header.tsx @@ -15,8 +15,6 @@ import { setPreference, ThemeToggle, } from '../theme-toggle/theme-toggle'; -import BuilderContentComp from '../../components/builder-content'; -import { BUILDER_TOP_BAR_MODEL, BUILDER_PUBLIC_API_KEY } from '../../constants'; export const Header = component$(() => { useStyles$(styles); @@ -31,23 +29,8 @@ export const Header = component$(() => { }); }); - const hasBuilderBar = !( - pathname.startsWith('/examples') || - pathname.startsWith('/tutorial') || - pathname.startsWith('/playground') - ); - return ( <> - {hasBuilderBar && ( -
    - -
    - )}
    { @@ -18,9 +18,9 @@ const snarkdownEnhanced = (md: string) => { export const QwikGPT = component$((props: { query: string }) => { const message = useSignal(''); - const done = useSignal(false); - const id = useSignal(); - const rated = useSignal(false); + // const done = useSignal(false); + // const id = useSignal(); + // const rated = useSignal(false); const process = useComputed$(() => { const rawLines = message.value.split('\n'); @@ -70,30 +70,30 @@ export const QwikGPT = component$((props: { query: string }) => { return lines; }); - useTask$(({ track }) => { - const query = track(() => props.query); - if (isBrowser) { - message.value = ''; - done.value = false; - rated.value = false; - id.value = undefined; - if (props.query !== '') { - const run = async () => { - done.value = false; - const response = await qwikGPT(query); - for await (const value of response) { - if (typeof value === 'string') { - message.value += value; - } else if (value.type === 'id') { - id.value = value.content; - } - } - done.value = true; - }; - run(); - } - } - }); + // useTask$(({ track }) => { + // const query = track(() => props.query); + // if (isBrowser) { + // message.value = ''; + // done.value = false; + // rated.value = false; + // id.value = undefined; + // if (props.query !== '') { + // const run = async () => { + // done.value = false; + // const response = await qwikGPT(query); + // for await (const value of response) { + // if (typeof value === 'string') { + // message.value += value; + // } else if (value.type === 'id') { + // id.value = value.content; + // } + // } + // done.value = true; + // }; + // run(); + // } + // } + // }); if (message.value === '' && props.query !== '') { return ( @@ -114,7 +114,7 @@ export const QwikGPT = component$((props: { query: string }) => { ); })} - {done.value && ( + {/* {done.value && (
    {rated.value ? ( <>Thank you very much! @@ -142,7 +142,7 @@ export const QwikGPT = component$((props: { query: string }) => { )}
    - )} + )} */} ); }); diff --git a/packages/docs/src/components/qwik-gpt/search.tsx b/packages/docs/src/components/qwik-gpt/search.tsx index b69b1acf2ec..416e1325e97 100644 --- a/packages/docs/src/components/qwik-gpt/search.tsx +++ b/packages/docs/src/components/qwik-gpt/search.tsx @@ -1,117 +1,117 @@ -import { server$ } from '@builder.io/qwik-city'; -import { createClient } from '@supabase/supabase-js'; +// import { server$ } from '@builder.io/qwik-city'; +// import { createClient } from '@supabase/supabase-js'; import gpt from './gpt.md?raw'; -import { chatCompletion } from './streaming-gpt'; +// import { chatCompletion } from './streaming-gpt'; const files = new Map>(); -export const qwikGPT = server$(async function* (query: string) { - const supabase = createClient(this.env.get('SUPABASE_URL')!, this.env.get('SUPABASE_KEY')!); - const normalizedQuery = normalizeLine(query); - const response = await fetch('https://api.openai.com/v1/embeddings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${this.env.get('OPENAI_KEY')}`, - }, - body: JSON.stringify({ - input: normalizedQuery, - model: 'text-embedding-ada-002', - }), - }); - const data = await response.json(); - const embeddings = data.data[0].embedding; - - const docs = await supabase.rpc('match_docs_10', { - query_text: normalizedQuery.replaceAll(' ', '|'), - query_embedding: embeddings, - match_count: 6, - similarity_threshold: 0.79, - }); - - const resultsHash = await getResultsHash(docs.data); - const existingQuery = await supabase.rpc('match_query_3', { - query_embedding: embeddings, - similarity_threshold: 0.95, - hash: resultsHash, - }); - if (existingQuery.data.length === 1) { - const entry = existingQuery.data[0]; - yield { - type: 'id', - content: entry.id, - }; - yield entry.output; - return; - } - - // Download docs - try { - const docsStr = await resolveContext(docs.data); - let model = 'gpt-4'; - if (docsStr.length < 3500 * 3 && Math.random() < 0.5) { - model = 'gpt-3.5-turbo'; - } - const insert = supabase - .from('search_queries') - .insert({ - query: query, - embedding: embeddings, - results: docs.data, - results_hash: resultsHash, - model, - }) - .select('id'); - - const id = (await insert).data?.[0].id as string; - yield { - type: 'id', - content: id, - }; - - if (docs.data.length === 0) { - yield 'We could not find any documentation that matches your question. Please try again rephrasing your question to be more factual.'; - return; - } - - const generator = chatCompletion(this.env.get('OPENAI_KEY')!, { - model: model, - temperature: 0, - messages: [ - { - role: 'system', - content: - 'You are QwikGPT, your job is to answer questions about Qwik, a new javascript framework focused on instant interactivity and server-side rendering.\nRelevant Qwik documentation and the user question will be provided. Try to answer the question in a short and concise way.', - }, - { - role: 'user', - content: docsStr, - }, - { - role: 'user', - content: `User question, respond in markdown including links to the sources, if you are not sure about the answer, say that you do not know:\n\n${query}`, - }, - ], - }); - - let output = ''; - for await (const chunk of generator) { - output += chunk; - yield chunk as string; - } - await supabase.from('search_queries').update({ output }).eq('id', id); - } catch (e) { - console.error(e); - } -}); - -export const rateResponse = server$(async function (query_id: string, rate: number) { - const supabase = createClient(this.env.get('SUPABASE_URL')!, this.env.get('SUPABASE_KEY')!); - await supabase.from('search_rate').insert({ - query_id: query_id, - rate: rate, - }); -}); +// export const qwikGPT = server$(async function* (query: string) { +// const supabase = createClient(this.env.get('SUPABASE_URL')!, this.env.get('SUPABASE_KEY')!); +// const normalizedQuery = normalizeLine(query); +// const response = await fetch('https://api.openai.com/v1/embeddings', { +// method: 'POST', +// headers: { +// 'Content-Type': 'application/json', +// Authorization: `Bearer ${this.env.get('OPENAI_KEY')}`, +// }, +// body: JSON.stringify({ +// input: normalizedQuery, +// model: 'text-embedding-ada-002', +// }), +// }); +// const data = await response.json(); +// const embeddings = data.data[0].embedding; + +// const docs = await supabase.rpc('match_docs_10', { +// query_text: normalizedQuery.replaceAll(' ', '|'), +// query_embedding: embeddings, +// match_count: 6, +// similarity_threshold: 0.79, +// }); + +// const resultsHash = await getResultsHash(docs.data); +// const existingQuery = await supabase.rpc('match_query_3', { +// query_embedding: embeddings, +// similarity_threshold: 0.95, +// hash: resultsHash, +// }); +// if (existingQuery.data.length === 1) { +// const entry = existingQuery.data[0]; +// yield { +// type: 'id', +// content: entry.id, +// }; +// yield entry.output; +// return; +// } + +// // Download docs +// try { +// const docsStr = await resolveContext(docs.data); +// let model = 'gpt-4'; +// if (docsStr.length < 3500 * 3 && Math.random() < 0.5) { +// model = 'gpt-3.5-turbo'; +// } +// const insert = supabase +// .from('search_queries') +// .insert({ +// query: query, +// embedding: embeddings, +// results: docs.data, +// results_hash: resultsHash, +// model, +// }) +// .select('id'); + +// const id = (await insert).data?.[0].id as string; +// yield { +// type: 'id', +// content: id, +// }; + +// if (docs.data.length === 0) { +// yield 'We could not find any documentation that matches your question. Please try again rephrasing your question to be more factual.'; +// return; +// } + +// const generator = chatCompletion(this.env.get('OPENAI_KEY')!, { +// model: model, +// temperature: 0, +// messages: [ +// { +// role: 'system', +// content: +// 'You are QwikGPT, your job is to answer questions about Qwik, a new javascript framework focused on instant interactivity and server-side rendering.\nRelevant Qwik documentation and the user question will be provided. Try to answer the question in a short and concise way.', +// }, +// { +// role: 'user', +// content: docsStr, +// }, +// { +// role: 'user', +// content: `User question, respond in markdown including links to the sources, if you are not sure about the answer, say that you do not know:\n\n${query}`, +// }, +// ], +// }); + +// let output = ''; +// for await (const chunk of generator) { +// output += chunk; +// yield chunk as string; +// } +// await supabase.from('search_queries').update({ output }).eq('id', id); +// } catch (e) { +// console.error(e); +// } +// }); + +// export const rateResponse = server$(async function (query_id: string, rate: number) { +// const supabase = createClient(this.env.get('SUPABASE_URL')!, this.env.get('SUPABASE_KEY')!); +// await supabase.from('search_rate').insert({ +// query_id: query_id, +// rate: rate, +// }); +// }); export function normalizeLine(line: string) { line = line.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); diff --git a/packages/docs/src/components/svgs/discord-logo.tsx b/packages/docs/src/components/svgs/discord-logo.tsx index a11898ceb2d..a550600ffbc 100644 --- a/packages/docs/src/components/svgs/discord-logo.tsx +++ b/packages/docs/src/components/svgs/discord-logo.tsx @@ -1,7 +1,9 @@ -interface DiscordLogoProps { +import type { PropsOf } from '@builder.io/qwik'; + +type DiscordLogoProps = { width: number; height: number; -} +} & PropsOf<'svg'>; export const DiscordLogo = ({ width, height }: DiscordLogoProps) => ( ; export const QwikLogo = component$((props: QwikLogoProps) => { return ( <> { xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Qwik Logo" - class="qwik-logo" + class={['qwik-logo', props.class]} > { +const blobUrl = (code: string, type: string = 'application/javascript') => { if (isServer) { return ''; } - const blob = new Blob([code], { type: 'application/javascript' }); + const blob = new Blob([code], { type }); return URL.createObjectURL(blob); }; @@ -63,6 +67,8 @@ const bundled: PkgUrls = { '/optimizer.cjs': blobUrl(qOptimizerCjs), '/server.cjs': blobUrl(qServerCjs), '/server.d.ts': blobUrl(qServerDts), + '/bindings/qwik.wasm.cjs': blobUrl(qWasmCjs), + '/bindings/qwik_wasm_bg.wasm': qWasmBinUrl, }, prettier: { version: prettierPkgJson.version, diff --git a/packages/docs/src/repl/repl.tsx b/packages/docs/src/repl/repl.tsx index 4d1113dcdd9..593f1a30b64 100644 --- a/packages/docs/src/repl/repl.tsx +++ b/packages/docs/src/repl/repl.tsx @@ -177,6 +177,18 @@ const getDependencies = (input: ReplAppInput) => { '/core.min.mjs': getNpmCdnUrl(bundled, QWIK_PKG_NAME, input.version, '/core.min.mjs'), '/optimizer.cjs': getNpmCdnUrl(bundled, QWIK_PKG_NAME, input.version, '/optimizer.cjs'), '/server.cjs': getNpmCdnUrl(bundled, QWIK_PKG_NAME, input.version, '/server.cjs'), + '/bindings/qwik.wasm.cjs': getNpmCdnUrl( + bundled, + QWIK_PKG_NAME, + input.version, + '/bindings/qwik.wasm.cjs' + ), + '/bindings/qwik_wasm_bg.wasm': getNpmCdnUrl( + bundled, + QWIK_PKG_NAME, + input.version, + '/bindings/qwik_wasm_bg.wasm' + ), }; } return out; diff --git a/packages/docs/src/repl/worker/repl-dependencies.ts b/packages/docs/src/repl/worker/repl-dependencies.ts index ec08dd13443..b4dd5e2cd0d 100644 --- a/packages/docs/src/repl/worker/repl-dependencies.ts +++ b/packages/docs/src/repl/worker/repl-dependencies.ts @@ -37,6 +37,7 @@ const exec = async (pkgName: string, pkgPath: string) => { const _loadDependencies = async (replOptions: ReplInputOptions) => { options = replOptions; const qwikVersion = options.version; + const realQwikVersion = options.deps[QWIK_PKG_NAME].version; cache = await caches.open(QWIK_REPL_DEPS_CACHE); @@ -46,6 +47,22 @@ const _loadDependencies = async (replOptions: ReplInputOptions) => { isDev: false, }; + const cachedCjsCode = `qwikWasmCjs${realQwikVersion}`; + const cachedWasmRsp = `qwikWasmRsp${realQwikVersion}`; + + // Store the optimizer where platform.ts can find it + let cjsCode: string = (globalThis as any)[cachedCjsCode]; + let wasmRsp: Response = (globalThis as any)[cachedWasmRsp]; + if (!cjsCode || !wasmRsp) { + const cjsRes = await depResponse(QWIK_PKG_NAME, '/bindings/qwik.wasm.cjs'); + cjsCode = await cjsRes.text(); + (globalThis as any)[cachedCjsCode] = cjsCode; + const res = await depResponse(QWIK_PKG_NAME, '/bindings/qwik_wasm_bg.wasm'); + wasmRsp = res; + (globalThis as any)[cachedWasmRsp] = wasmRsp; + console.debug(`Loaded Qwik WASM bindings ${realQwikVersion}`); + } + if (!isSameQwikVersion(self.qwikCore?.version)) { await exec(QWIK_PKG_NAME, '/core.cjs'); if (self.qwikCore) { diff --git a/packages/docs/src/routes/(ecosystem)/showcase/generated-pages.json b/packages/docs/src/routes/(ecosystem)/showcase/generated-pages.json index 04cedbb6621..dd45294c0f4 100644 --- a/packages/docs/src/routes/(ecosystem)/showcase/generated-pages.json +++ b/packages/docs/src/routes/(ecosystem)/showcase/generated-pages.json @@ -16,6 +16,22 @@ "size": "large", "tags": "site,healthcare,services,webrtc,websocket" }, + { + "title": "Ohayo Dev & Design | Acceuil", + "imgSrc": "/showcases/ohayo-dev-design_com_.webp", + "perf": { + "score": 0.98, + "fcpDisplay": "1.1 s", + "fcpScore": 1, + "lcpDisplay": "2.3 s", + "lcpScore": 0.93, + "ttiDisplay": "1.4 s", + "ttiScore": 1, + "ttiTime": 1370.12221275 + }, + "href": "https://ohayo-dev-design.com/", + "tags": "web, dev, agency" + }, { "title": "Wep Apps | Zhou's Portfolio", "imgSrc": "/showcases/linkfang-portfolio_vercel_app_.webp", diff --git a/packages/docs/src/routes/(shop)/shop/shop-product.tsx b/packages/docs/src/routes/(shop)/shop/shop-product.tsx index 41045e98edd..a3a306ad0ee 100644 --- a/packages/docs/src/routes/(shop)/shop/shop-product.tsx +++ b/packages/docs/src/routes/(shop)/shop/shop-product.tsx @@ -2,7 +2,7 @@ import { component$, useComputed$, useContext, useSignal } from '@builder.io/qwi import { modifyLineItemMutation } from '../mutation'; import { SHOP_CONTEXT, fetchFromShopify, formatPrice } from '../utils'; import type { UIProduct } from '../types'; -import { Image } from '@unpic/qwik'; +import { Image } from 'qwik-image'; type Props = { product: UIProduct; diff --git a/packages/docs/src/routes/api/qwik-city/api.json b/packages/docs/src/routes/api/qwik-city/api.json index 656675f37ae..92df4b4f318 100644 --- a/packages/docs/src/routes/api/qwik-city/api.json +++ b/packages/docs/src/routes/api/qwik-city/api.json @@ -670,7 +670,7 @@ } ], "kind": "Function", - "content": "```typescript\nserver$: (first: T, options?: ServerConfig | undefined) => ServerQRL\n```\n\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nfirst\n\n\n\n\nT\n\n\n\n\n\n
    \n\noptions\n\n\n\n\nServerConfig \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
    \n**Returns:**\n\n[ServerQRL](#serverqrl)<T>", + "content": "```typescript\nserver$: (qrl: T, options?: ServerConfig | undefined) => ServerQRL\n```\n\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nqrl\n\n\n\n\nT\n\n\n\n\n\n
    \n\noptions\n\n\n\n\nServerConfig \\| undefined\n\n\n\n\n_(Optional)_\n\n\n
    \n**Returns:**\n\n[ServerQRL](#serverqrl)<T>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-city/runtime/src/server-functions.ts", "mdFile": "qwik-city.server_.md" }, diff --git a/packages/docs/src/routes/api/qwik-city/index.md b/packages/docs/src/routes/api/qwik-city/index.md index 54dc0484708..fcadbe6425c 100644 --- a/packages/docs/src/routes/api/qwik-city/index.md +++ b/packages/docs/src/routes/api/qwik-city/index.md @@ -2129,7 +2129,7 @@ RouterOutlet: import("@builder.io/qwik").Component; ```typescript server$: ( - first: T, + qrl: T, options?: ServerConfig | undefined, ) => ServerQRL; ``` @@ -2149,7 +2149,7 @@ Description -first +qrl diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index 22ede9ec353..b2354f7ea5d 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -506,6 +506,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/component/component.public.ts", "mdFile": "qwik.componentqrl.md" }, + { + "name": "ComputedSignal2", + "id": "computedsignal2", + "hierarchy": [ + { + "name": "ComputedSignal2", + "id": "computedsignal2" + } + ], + "kind": "Interface", + "content": "```typescript\nexport interface ComputedSignal2 extends ReadonlySignal2 \n```\n**Extends:** [ReadonlySignal2](#readonlysignal2)<T>\n\n\n\n\n
    \n\nMethod\n\n\n\n\nDescription\n\n\n
    \n\n[force()](#computedsignal2-force)\n\n\n\n\nUse this to force recalculation and running subscribers, for example when the calculated value mutates but remains the same object. Useful for third-party libraries.\n\n\n
    ", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts", + "mdFile": "qwik.computedsignal2.md" + }, { "name": "ContextId", "id": "contextid", @@ -548,6 +562,34 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts", "mdFile": "qwik.correctedtoggleevent.md" }, + { + "name": "createComputed2$", + "id": "createcomputed2_", + "hierarchy": [ + { + "name": "createComputed2$", + "id": "createcomputed2_" + } + ], + "kind": "Function", + "content": "```typescript\ncreateComputed2$: (first: () => T) => ComputedSignal2\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nfirst\n\n\n\n\n() => T\n\n\n\n\n\n
    \n**Returns:**\n\n[ComputedSignal2](#computedsignal2)<T>", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts", + "mdFile": "qwik.createcomputed2_.md" + }, + { + "name": "createComputed2Qrl", + "id": "createcomputed2qrl", + "hierarchy": [ + { + "name": "createComputed2Qrl", + "id": "createcomputed2qrl" + } + ], + "kind": "Function", + "content": "```typescript\ncreateComputed2Qrl: (qrl: QRL<() => T>) => ComputedSignal2\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nqrl\n\n\n\n\n[QRL](#qrl)<() => T>\n\n\n\n\n\n
    \n**Returns:**\n\n[ComputedSignal2](#computedsignal2)<T>", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts", + "mdFile": "qwik.createcomputed2qrl.md" + }, { "name": "createContextId", "id": "createcontextid", @@ -562,6 +604,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts", "mdFile": "qwik.createcontextid.md" }, + { + "name": "createSignal2", + "id": "createsignal2", + "hierarchy": [ + { + "name": "createSignal2", + "id": "createsignal2" + } + ], + "kind": "Variable", + "content": "```typescript\ncreateSignal2: {\n (): Signal2;\n (value: T): Signal2;\n}\n```", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts", + "mdFile": "qwik.createsignal2.md" + }, { "name": "CSSProperties", "id": "cssproperties", @@ -809,6 +865,23 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-generated.ts", "mdFile": "qwik.fieldsethtmlattributes.md" }, + { + "name": "force", + "id": "computedsignal2-force", + "hierarchy": [ + { + "name": "ComputedSignal2", + "id": "computedsignal2-force" + }, + { + "name": "force", + "id": "computedsignal2-force" + } + ], + "kind": "MethodSignature", + "content": "Use this to force recalculation and running subscribers, for example when the calculated value mutates but remains the same object. Useful for third-party libraries.\n\n\n```typescript\nforce(): void;\n```\n**Returns:**\n\nvoid", + "mdFile": "qwik.computedsignal2.force.md" + }, { "name": "FormHTMLAttributes", "id": "formhtmlattributes", @@ -1057,7 +1130,7 @@ } ], "kind": "Function", - "content": "Create a `____$(...)` convenience method from `___(...)`.\n\nIt is very common for functions to take a lazy-loadable resource as a first argument. For this reason, the Qwik Optimizer automatically extracts the first argument from any function which ends in `$`.\n\nThis means that `foo$(arg0)` and `foo($(arg0))` are equivalent with respect to Qwik Optimizer. The former is just a shorthand for the latter.\n\nFor example, these function calls are equivalent:\n\n- `component$(() => {...})` is same as `component($(() => {...}))`\n\n```tsx\nexport function myApi(callback: QRL<() => void>): void {\n // ...\n}\n\nexport const myApi$ = implicit$FirstArg(myApi);\n// type of myApi$: (callback: () => void): void\n\n// can be used as:\nmyApi$(() => console.log('callback'));\n\n// will be transpiled to:\n// FILE: \nmyApi(qrl('./chunk-abc.js', 'callback'));\n\n// FILE: chunk-abc.js\nexport const callback = () => console.log('callback');\n```\n\n\n```typescript\nimplicit$FirstArg: (fn: (first: QRL, ...rest: REST) => RET) => (first: FIRST, ...rest: REST) => RET\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nfn\n\n\n\n\n(first: [QRL](#qrl)<FIRST>, ...rest: REST) => RET\n\n\n\n\nA function that should have its first argument automatically `$`.\n\n\n
    \n**Returns:**\n\n(first: FIRST, ...rest: REST) => RET", + "content": "Create a `____$(...)` convenience method from `___(...)`.\n\nIt is very common for functions to take a lazy-loadable resource as a first argument. For this reason, the Qwik Optimizer automatically extracts the first argument from any function which ends in `$`.\n\nThis means that `foo$(arg0)` and `foo($(arg0))` are equivalent with respect to Qwik Optimizer. The former is just a shorthand for the latter.\n\nFor example, these function calls are equivalent:\n\n- `component$(() => {...})` is same as `component($(() => {...}))`\n\n```tsx\nexport function myApi(callback: QRL<() => void>): void {\n // ...\n}\n\nexport const myApi$ = implicit$FirstArg(myApi);\n// type of myApi$: (callback: () => void): void\n\n// can be used as:\nmyApi$(() => console.log('callback'));\n\n// will be transpiled to:\n// FILE: \nmyApi(qrl('./chunk-abc.js', 'callback'));\n\n// FILE: chunk-abc.js\nexport const callback = () => console.log('callback');\n```\n\n\n```typescript\nimplicit$FirstArg: (fn: (qrl: QRL, ...rest: REST) => RET) => ((qrl: FIRST, ...rest: REST) => RET)\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nfn\n\n\n\n\n(qrl: [QRL](#qrl)<FIRST>, ...rest: REST) => RET\n\n\n\n\nA function that should have its first argument automatically `$`.\n\n\n
    \n**Returns:**\n\n((qrl: FIRST, ...rest: REST) => RET)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/util/implicit_dollar.ts", "mdFile": "qwik.implicit_firstarg.md" }, @@ -1130,7 +1203,7 @@ } ], "kind": "Function", - "content": "```typescript\nisSignal2: (value: any) => value is ISignal2\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nvalue\n\n\n\n\nany\n\n\n\n\n\n
    \n**Returns:**\n\nvalue is ISignal2<unknown>", + "content": "```typescript\nisSignal2: (value: any) => value is ISignal2\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nvalue\n\n\n\n\nany\n\n\n\n\n\n
    \n**Returns:**\n\nvalue is [ISignal2](#signal2)<unknown>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.ts", "mdFile": "qwik.issignal.md" }, @@ -2212,6 +2285,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/state/signal.ts", "mdFile": "qwik.readonlysignal.md" }, + { + "name": "ReadonlySignal2", + "id": "readonlysignal2", + "hierarchy": [ + { + "name": "ReadonlySignal2", + "id": "readonlysignal2" + } + ], + "kind": "Interface", + "content": "```typescript\nexport interface ReadonlySignal2 \n```\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[untrackedValue](#)\n\n\n\n\n`readonly`\n\n\n\n\nT\n\n\n\n\n\n
    \n\n[value](#)\n\n\n\n\n`readonly`\n\n\n\n\nT\n\n\n\n\n\n
    ", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts", + "mdFile": "qwik.readonlysignal2.md" + }, { "name": "render", "id": "render", @@ -2460,10 +2547,24 @@ } ], "kind": "Interface", - "content": "```typescript\nexport interface Signal \n```\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[value](#)\n\n\n\n\n\n\n\nT\n\n\n\n\n\n
    ", + "content": "A signal is a reactive value which can be read and written. When the signal is written, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered.\n\nFurthermore, when a signal value is passed as a prop to a component, the optimizer will automatically forward the signal. This means that `return
    hi
    ` will update the `title` attribute when the signal changes without having to re-render the component.\n\n\n```typescript\nexport interface Signal \n```\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[value](#)\n\n\n\n\n\n\n\nT\n\n\n\n\n\n
    ", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/state/signal.ts", "mdFile": "qwik.signal.md" }, + { + "name": "Signal2", + "id": "signal2", + "hierarchy": [ + { + "name": "Signal2", + "id": "signal2" + } + ], + "kind": "Interface", + "content": "```typescript\nexport interface Signal2 extends ReadonlySignal2 \n```\n**Extends:** [ReadonlySignal2](#readonlysignal2)<T>\n\n\n\n\n\n
    \n\nProperty\n\n\n\n\nModifiers\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\n[untrackedValue](#)\n\n\n\n\n\n\n\nT\n\n\n\n\n\n
    \n\n[value](#)\n\n\n\n\n\n\n\nT\n\n\n\n\n\n
    ", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts", + "mdFile": "qwik.signal2.md" + }, { "name": "Size", "id": "size", @@ -2968,6 +3069,20 @@ "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts", "mdFile": "qwik.usecomputedqrl.md" }, + { + "name": "useConstant", + "id": "useconstant", + "hierarchy": [ + { + "name": "useConstant", + "id": "useconstant" + } + ], + "kind": "Function", + "content": "> Warning: This API is now obsolete.\n> \n> This is a technology preview\n> \n\nStores a value which is retained for the lifetime of the component.\n\nIf the value is a function, the function is invoked to calculate the actual value.\n\n\n```typescript\nuseConstant: (value: (() => T) | T) => T\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nvalue\n\n\n\n\n(() => T) \\| T\n\n\n\n\n\n
    \n**Returns:**\n\nT", + "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts", + "mdFile": "qwik.useconstant.md" + }, { "name": "useContext", "id": "usecontext", @@ -3090,7 +3205,7 @@ } ], "kind": "Function", - "content": "This method works like an async memoized function that runs whenever some tracked value changes and returns some data.\n\n`useResource` however returns immediate a `ResourceReturn` object that contains the data and a state that indicates if the data is available or not.\n\nThe status can be one of the following:\n\n- 'pending' - the data is not yet available. - 'resolved' - the data is available. - 'rejected' - the data is not available due to an error or timeout.\n\n\\#\\#\\# Example\n\nExample showing how `useResource` to perform a fetch to request the weather, whenever the input city name changes.\n\n```tsx\nconst Cmp = component$(() => {\n const cityS = useSignal('');\n\n const weatherResource = useResource$(async ({ track, cleanup }) => {\n const cityName = track(cityS);\n const abortController = new AbortController();\n cleanup(() => abortController.abort('cleanup'));\n const res = await fetch(`http://weatherdata.com?city=${cityName}`, {\n signal: abortController.signal,\n });\n const data = await res.json();\n return data as { temp: number };\n });\n\n return (\n
    \n \n {\n return
    Temperature: {weather.temp}
    ;\n }}\n />\n
    \n );\n});\n```\n\n\n```typescript\nuseResourceQrl: (qrl: QRL>, opts?: ResourceOptions) => ResourceReturn\n```\n\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nqrl\n\n\n\n\n[QRL](#qrl)<[ResourceFn](#resourcefn)<T>>\n\n\n\n\n\n
    \n\nopts\n\n\n\n\n[ResourceOptions](#resourceoptions)\n\n\n\n\n_(Optional)_\n\n\n
    \n**Returns:**\n\n[ResourceReturn](#resourcereturn)<T>", + "content": "This method works like an async memoized function that runs whenever some tracked value changes and returns some data.\n\n`useResource` however returns immediate a `ResourceReturn` object that contains the data and a state that indicates if the data is available or not.\n\nThe status can be one of the following:\n\n- `pending` - the data is not yet available. - `resolved` - the data is available. - `rejected` - the data is not available due to an error or timeout.\n\nAvoid using a `try/catch` statement in `useResource$`. If you catch the error instead of passing it, the resource status will never be `rejected`.\n\n\\#\\#\\# Example\n\nExample showing how `useResource` to perform a fetch to request the weather, whenever the input city name changes.\n\n```tsx\nconst Cmp = component$(() => {\n const cityS = useSignal('');\n\n const weatherResource = useResource$(async ({ track, cleanup }) => {\n const cityName = track(cityS);\n const abortController = new AbortController();\n cleanup(() => abortController.abort('cleanup'));\n const res = await fetch(`http://weatherdata.com?city=${cityName}`, {\n signal: abortController.signal,\n });\n const data = await res.json();\n return data as { temp: number };\n });\n\n return (\n
    \n \n {\n return
    Temperature: {weather.temp}
    ;\n }}\n />\n
    \n );\n});\n```\n\n\n```typescript\nuseResourceQrl: (qrl: QRL>, opts?: ResourceOptions) => ResourceReturn\n```\n\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nqrl\n\n\n\n\n[QRL](#qrl)<[ResourceFn](#resourcefn)<T>>\n\n\n\n\n\n
    \n\nopts\n\n\n\n\n[ResourceOptions](#resourceoptions)\n\n\n\n\n_(Optional)_\n\n\n
    \n**Returns:**\n\n[ResourceReturn](#resourcereturn)<T>", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-resource.ts", "mdFile": "qwik.useresourceqrl.md" }, @@ -3118,7 +3233,7 @@ } ], "kind": "Variable", - "content": "```typescript\nuseSignal: UseSignal\n```", + "content": "Hook that creates a signal that is retained for the lifetime of the component.\n\n\n```typescript\nuseSignal: UseSignal\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts", "mdFile": "qwik.usesignal.md" }, @@ -3132,7 +3247,7 @@ } ], "kind": "Interface", - "content": "```typescript\nuseSignal: UseSignal\n```", + "content": "Hook that creates a signal that is retained for the lifetime of the component.\n\n\n```typescript\nuseSignal: UseSignal\n```", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts", "mdFile": "qwik.usesignal.md" }, @@ -3174,7 +3289,7 @@ } ], "kind": "Function", - "content": "A lazy-loadable reference to a component's styles.\n\nComponent styles allow Qwik to lazy load the style information for the component only when needed. (And avoid double loading it in case of SSR hydration.)\n\n```tsx\nimport styles from './code-block.css?inline';\n\nexport const CmpStyles = component$(() => {\n useStyles$(styles);\n\n return
    Some text
    ;\n});\n```\n\n\n```typescript\nuseStyles$: (first: string) => UseStyles\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nfirst\n\n\n\n\nstring\n\n\n\n\n\n
    \n**Returns:**\n\nUseStyles", + "content": "A lazy-loadable reference to a component's styles.\n\nComponent styles allow Qwik to lazy load the style information for the component only when needed. (And avoid double loading it in case of SSR hydration.)\n\n```tsx\nimport styles from './code-block.css?inline';\n\nexport const CmpStyles = component$(() => {\n useStyles$(styles);\n\n return
    Some text
    ;\n});\n```\n\n\n```typescript\nuseStyles$: (qrl: string) => UseStyles\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nqrl\n\n\n\n\nstring\n\n\n\n\n\n
    \n**Returns:**\n\nUseStyles", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-styles.ts", "mdFile": "qwik.usestyles_.md" }, @@ -3216,7 +3331,7 @@ } ], "kind": "Function", - "content": "A lazy-loadable reference to a component's styles, that is scoped to the component.\n\nComponent styles allow Qwik to lazy load the style information for the component only when needed. (And avoid double loading it in case of SSR hydration.)\n\n```tsx\nimport scoped from './code-block.css?inline';\n\nexport const CmpScopedStyles = component$(() => {\n useStylesScoped$(scoped);\n\n return
    Some text
    ;\n});\n```\n\n\n```typescript\nuseStylesScoped$: (first: string) => UseStylesScoped\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nfirst\n\n\n\n\nstring\n\n\n\n\n\n
    \n**Returns:**\n\n[UseStylesScoped](#usestylesscoped)", + "content": "A lazy-loadable reference to a component's styles, that is scoped to the component.\n\nComponent styles allow Qwik to lazy load the style information for the component only when needed. (And avoid double loading it in case of SSR hydration.)\n\n```tsx\nimport scoped from './code-block.css?inline';\n\nexport const CmpScopedStyles = component$(() => {\n useStylesScoped$(scoped);\n\n return
    Some text
    ;\n});\n```\n\n\n```typescript\nuseStylesScoped$: (qrl: string) => UseStylesScoped\n```\n\n\n\n\n
    \n\nParameter\n\n\n\n\nType\n\n\n\n\nDescription\n\n\n
    \n\nqrl\n\n\n\n\nstring\n\n\n\n\n\n
    \n**Returns:**\n\n[UseStylesScoped](#usestylesscoped)", "editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-styles.ts", "mdFile": "qwik.usestylesscoped_.md" }, diff --git a/packages/docs/src/routes/api/qwik/index.md b/packages/docs/src/routes/api/qwik/index.md index e74d7eca504..bd0ab883c95 100644 --- a/packages/docs/src/routes/api/qwik/index.md +++ b/packages/docs/src/routes/api/qwik/index.md @@ -1408,6 +1408,36 @@ componentQrl [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/component/component.public.ts) +## ComputedSignal2 + +```typescript +export interface ComputedSignal2 extends ReadonlySignal2 +``` + +**Extends:** [ReadonlySignal2](#readonlysignal2)<T> + + + +
    + +Method + + + +Description + +
    + +[force()](#computedsignal2-force) + + + +Use this to force recalculation and running subscribers, for example when the calculated value mutates but remains the same object. Useful for third-party libraries. + +
    + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts) + ## ContextId ContextId is a typesafe ID for your context. @@ -1688,6 +1718,80 @@ Description [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-qwik-attributes.ts) +## createComputed2$ + +```typescript +createComputed2$: (first: () => T) => ComputedSignal2; +``` + + + +
    + +Parameter + + + +Type + + + +Description + +
    + +first + + + +() => T + + + +
    +**Returns:** + +[ComputedSignal2](#computedsignal2)<T> + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts) + +## createComputed2Qrl + +```typescript +createComputed2Qrl: (qrl: QRL<() => T>) => ComputedSignal2; +``` + + + +
    + +Parameter + + + +Type + + + +Description + +
    + +qrl + + + +[QRL](#qrl)<() => T> + + + +
    +**Returns:** + +[ComputedSignal2](#computedsignal2)<T> + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts) + ## createContextId Create a context ID to be used in your application. The name should be written with no spaces. @@ -1769,6 +1873,17 @@ The name of the context. [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts) +## createSignal2 + +```typescript +createSignal2: { + (): Signal2; + (value: T): Signal2; +} +``` + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts) + ## CSSProperties ```typescript @@ -2079,7 +2194,7 @@ Description -first +qrl @@ -2154,6 +2269,18 @@ export interface FieldsetHTMLAttributes extends Attrs<'fields [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/render/jsx/types/jsx-generated.ts) +## force + +Use this to force recalculation and running subscribers, for example when the calculated value mutates but remains the same object. Useful for third-party libraries. + +```typescript +force(): void; +``` + +**Returns:** + +void + ## FormHTMLAttributes ```typescript @@ -2532,9 +2659,9 @@ export const callback = () => console.log("callback"); ```typescript implicit$FirstArg: ( - fn: (first: QRL, ...rest: REST) => RET, + fn: (qrl: QRL, ...rest: REST) => RET, ) => - (first: FIRST, ...rest: REST) => + (qrl: FIRST, ...rest: REST) => RET; ``` @@ -2557,7 +2684,7 @@ fn -(first: [QRL](#qrl)<FIRST>, ...rest: REST) => RET +(qrl: [QRL](#qrl)<FIRST>, ...rest: REST) => RET @@ -2567,7 +2694,7 @@ A function that should have its first argument automatically `$`. **Returns:** -(first: FIRST, ...rest: REST) => RET +((qrl: FIRST, ...rest: REST) => RET) [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/util/implicit_dollar.ts) @@ -2644,7 +2771,7 @@ any **Returns:** -value is ISignal2<unknown> +value is [ISignal2](#signal2)<unknown> [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.ts) @@ -4368,6 +4495,63 @@ export type ReadonlySignal = Readonly>; [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/state/signal.ts) +## ReadonlySignal2 + +```typescript +export interface ReadonlySignal2 +``` + + + + +
    + +Property + + + +Modifiers + + + +Type + + + +Description + +
    + +[untrackedValue](#) + + + +`readonly` + + + +T + + + +
    + +[value](#) + + + +`readonly` + + + +T + + + +
    + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts) + ## render Render JSX. @@ -5207,6 +5391,10 @@ plt ## Signal +A signal is a reactive value which can be read and written. When the signal is written, all tasks which are tracking the signal will be re-run and all components that read the signal will be re-rendered. + +Furthermore, when a signal value is passed as a prop to a component, the optimizer will automatically forward the signal. This means that `return
    hi
    ` will update the `title` attribute when the signal changes without having to re-render the component. + ```typescript export interface Signal ``` @@ -5245,6 +5433,61 @@ T [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/state/signal.ts) +## Signal2 + +```typescript +export interface Signal2 extends ReadonlySignal2 +``` + +**Extends:** [ReadonlySignal2](#readonlysignal2)<T> + + + + +
    + +Property + + + +Modifiers + + + +Type + + + +Description + +
    + +[untrackedValue](#) + + + + + +T + + + +
    + +[value](#) + + + + + +T + + + +
    + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/v2/signal/v2-signal.public.ts) + ## Size ```typescript @@ -10014,6 +10257,51 @@ useComputedQrl: ComputedQRL; [Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task.ts) +## useConstant + +> Warning: This API is now obsolete. +> +> This is a technology preview + +Stores a value which is retained for the lifetime of the component. + +If the value is a function, the function is invoked to calculate the actual value. + +```typescript +useConstant: (value: (() => T) | T) => T; +``` + + + +
    + +Parameter + + + +Type + + + +Description + +
    + +value + + + +(() => T) \| T + + + +
    +**Returns:** + +T + +[Edit this section](https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-signal.ts) + ## useContext Retrieve Context value. @@ -10434,7 +10722,9 @@ This method works like an async memoized function that runs whenever some tracke The status can be one of the following: -- 'pending' - the data is not yet available. - 'resolved' - the data is available. - 'rejected' - the data is not available due to an error or timeout. +- `pending` - the data is not yet available. - `resolved` - the data is available. - `rejected` - the data is not available due to an error or timeout. + +Avoid using a `try/catch` statement in `useResource$`. If you catch the error instead of passing it, the resource status will never be `rejected`. ### Example @@ -10557,6 +10847,8 @@ T \| undefined ## useSignal +Hook that creates a signal that is retained for the lifetime of the component. + ```typescript useSignal: UseSignal; ``` @@ -10565,6 +10857,8 @@ useSignal: UseSignal; ## UseSignal +Hook that creates a signal that is retained for the lifetime of the component. + ```typescript useSignal: UseSignal; ``` @@ -10753,7 +11047,7 @@ export const CmpStyles = component$(() => { ``` ```typescript -useStyles$: (first: string) => UseStyles; +useStyles$: (qrl: string) => UseStyles; ```
    @@ -10771,7 +11065,7 @@ Description
    -first +qrl @@ -10895,7 +11189,7 @@ export const CmpScopedStyles = component$(() => { ``` ```typescript -useStylesScoped$: (first: string) => UseStylesScoped; +useStylesScoped$: (qrl: string) => UseStylesScoped; ```
    @@ -10913,7 +11207,7 @@ Description
    -first +qrl @@ -11007,7 +11301,7 @@ Description
    -first +qrl @@ -11175,7 +11469,7 @@ Description
    -first +qrl diff --git a/packages/docs/src/routes/demo/integration/img/unpic/simple/index.tsx b/packages/docs/src/routes/demo/integration/img/unpic/simple/index.tsx deleted file mode 100644 index 915a2aa213e..00000000000 --- a/packages/docs/src/routes/demo/integration/img/unpic/simple/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { component$ } from '@builder.io/qwik'; -import { Image } from '@unpic/qwik'; - -export default component$(() => { - return ( - A lovely bath - ); -}); diff --git a/packages/docs/src/routes/demo/integration/modular-forms/index.tsx b/packages/docs/src/routes/demo/integration/modular-forms/index.tsx deleted file mode 100644 index aeb6dbfbb8d..00000000000 --- a/packages/docs/src/routes/demo/integration/modular-forms/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// @ts-nocheck -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { $, component$, type QRL } from '@builder.io/qwik'; -import { routeLoader$ } from '@builder.io/qwik-city'; -import type { InitialValues, SubmitHandler } from '@modular-forms/qwik'; -import { formAction$, useForm, valiForm$ } from '@modular-forms/qwik'; -import { email, type Input, minLength, object, string } from 'valibot'; - -const LoginSchema = object({ - email: string([ - minLength(1, 'Please enter your email.'), - email('The email address is badly formatted.'), - ]), - password: string([ - minLength(1, 'Please enter your password.'), - minLength(8, 'Your password must have 8 characters or more.'), - ]), -}); - -type LoginForm = Input; - -export const useFormLoader = routeLoader$>(() => ({ - email: '', - password: '', -})); - -export const useFormAction = formAction$((values) => { - // Runs on server -}, valiForm$(LoginSchema)); - -export default component$(() => { - const [loginForm, { Form, Field }] = useForm({ - loader: useFormLoader(), - action: useFormAction(), - validate: valiForm$(LoginSchema), - }); - - const handleSubmit: QRL> = $((values, event) => { - // Runs on client - console.log(values); - }); - - return ( -
    - - {(field, props) => ( -
    - - {field.error &&
    {field.error}
    } -
    - )} -
    - - {(field, props) => ( -
    - - {field.error &&
    {field.error}
    } -
    - )} -
    - -
    - ); -}); diff --git a/packages/docs/src/routes/docs/(qwik)/getting-started/index.mdx b/packages/docs/src/routes/docs/(qwik)/getting-started/index.mdx index 1b8fdf46cbf..d4ec2fdc3ae 100644 --- a/packages/docs/src/routes/docs/(qwik)/getting-started/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/getting-started/index.mdx @@ -52,7 +52,7 @@ To play with it right away, check out our in-browser playgrounds: To get started with Qwik locally, you need the following: -- [Node.js v16.8](https://nodejs.org/en/download/) or higher +- [Node.js v18.17](https://nodejs.org/en/download/) or higher - Your favorite IDE ([vscode](https://code.visualstudio.com/) recommended) - Optionally, read [think qwik](../concepts/think-qwik/index.mdx) diff --git a/packages/docs/src/routes/docs/(qwik)/index.mdx b/packages/docs/src/routes/docs/(qwik)/index.mdx index 17ceb0ea902..3b6564a1b01 100644 --- a/packages/docs/src/routes/docs/(qwik)/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/index.mdx @@ -72,19 +72,19 @@ Qwik is a new kind of web framework that can deliver instant loading web applica -## Qwik Goals +## Why Qwik?

    General-purpose

    -

    Qwik can be used to build any type of web site or application

    +

    Qwik can be used to build any type of website or application.

    -

    Instant-on

    -

    Unlike other frameworks, Qwik is resumable which means Qwik applications require 0 hydration. This allows Qwik apps to have instant-on interactivity, regardless of size or complexity.

    +

    Instant Interactivity

    +

    Qwik apps work instantly without any delay because they don't need hydration, regardless of their size or complexity.

    -

    Optimized for speed

    -

    Qwik has unprecedented performance, offering sub-second full page loads even on mobile devices. Qwik achieves this by delivering pure HTML, and incrementally loading JS only as-needed.

    +

    JS Streaming

    +

    Qwik delivers sub-second full page loads, even on mobile, by serving pure HTML and executing JavaScript when your users opt-in.

    diff --git a/packages/docs/src/routes/docs/(qwikcity)/pages/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/pages/index.mdx index 97ca718a747..ef8c10b0ccf 100644 --- a/packages/docs/src/routes/docs/(qwikcity)/pages/index.mdx +++ b/packages/docs/src/routes/docs/(qwikcity)/pages/index.mdx @@ -20,7 +20,7 @@ created_at: '2023-03-20T23:45:13Z' # Pages -Pages are created by adding a new `index.tsx` file in the `src/routes` directory. Pages exports a `default` Qwik component, which will be rendered as the content of the page. +Pages are created by adding a new `index.tsx` file in the `src/routes` directory. Pages export a `default` Qwik component, which will be rendered as the content of the page. ```tsx title="src/routes/some/path/index.tsx" import { component$ } from '@builder.io/qwik'; @@ -31,7 +31,7 @@ export default component$(() => { }); ``` -> The only difference between a page and an endpoint is that an endpoint only exports an `onRequest`, `onGet`, `onPost`, `onPut`, `onDelete`, `onPatch`, `onHead` function, which will be used to handle the incoming request. +> The only difference between a page and an endpoint is that an endpoint only exports an `onRequest`, `onGet`, `onPost`, `onPut`, `onDelete`, `onPatch`, or `onHead` function, which will be used to handle the incoming request. ## `head` export diff --git a/packages/docs/src/routes/docs/(qwikcity)/server$/index.mdx b/packages/docs/src/routes/docs/(qwikcity)/server$/index.mdx index 0a3ade9afb1..dcb633b98f9 100644 --- a/packages/docs/src/routes/docs/(qwikcity)/server$/index.mdx +++ b/packages/docs/src/routes/docs/(qwikcity)/server$/index.mdx @@ -20,15 +20,15 @@ created_at: '2023-03-29T02:35:29Z' # `server$()` -`server$()` allows you to create a function that always execute on the server, making it a great place to access the DB or perform server-only actions. +`server$()` allows you to define functions that execute exclusively on the server, making it ideal for server-only operations and database access. It functions as an RPC (Remote Procedure Call) mechanism between the client and server, similar to a traditional HTTP endpoint, but strongly typed with TypeScript and easier to maintain. -`server$` is a form of RPC (Remote Procedure Call) mechanism between the client and server, just like a traditional HTTP endpoint but strongly typed thanks to Typescript, and easier to maintain. +> `server$` can accept any number of arguments and return any value that can be serialized by Qwik, that includes primitives, objects, arrays, bigint, JSX nodes and even Promises, just to name a few. -Your new function will have the following signature: -`([AbortSignal, ] ...): Promise` -`AbortSignal` is optional, and allows you to cancel a long running request by terminating the connection. -Please note that depending on your server runtime, the function on the server may not terminate immediately. It depends on how client disconnections are handled by the runtime. +`AbortSignal` is optional, and allows you to cancel a long running request by terminating the connection. +Your new function will have the following signature: +`([AbortSignal, ...yourOtherArgs]): Promise` +> Please note that depending on your server runtime, the function on the server may not terminate immediately. It depends on how client disconnections are handled by the runtime. ```tsx import { component$, useSignal } from '@builder.io/qwik'; @@ -36,11 +36,13 @@ import { server$ } from '@builder.io/qwik-city'; // By wrapping a function with `server$()` we mark it to always // execute on the server. This is a form of RPC mechanism. -const serverGreeter = server$((firstName: string, lastName: string) => { - const greeting = `Hello ${firstName} ${lastName}`; - console.log('Prints in the server', greeting); - return greeting; -}); +export const serverGreeter = server$( + function (firstName: string, lastName: string) { + const greeting = `Hello ${firstName} ${lastName}`; + console.log('Prints in the server', greeting); + return greeting; + } +); export default component$(() => { const firstName = useSignal(''); @@ -52,10 +54,12 @@ export default component$(() => { @@ -64,57 +68,134 @@ export default component$(() => { }); ``` -`server$` can also read the HTTP cookies, headers, or environment variables, using `this`. In this case you will need to use a function instead of an arrow function. +## Accessing Request Information with `RequestEvent` + +When using `server$`, you have access to the `RequestEvent` object through `this`. This object provides useful information about the HTTP request, including environment variables, cookies, URL, and headers. Here's how you can use it: + +### Environment Variables + +You can access environment variables using `this.env.get()`. ```tsx -// Notice that the wrapped function is declared as an `async function` -const addUser = server$(async function(id: number, fullName: string, address: Address) { - // The `this` is the `RequestEvent` object, which contains - // the HTTP headers, cookies, and environment variables. - const db = createClient(this.env.get('DB_KEY')); - if (this.cookie.get('user-session')) { - await db.from('users').insert({ - id, - fullName, - address - }); - return { - success: true, +export const getEnvVariable = server$( + function () { + const dbKey = this.env.get('DB_KEY'); + console.log('Database Key:', dbKey); + return dbKey; + } +); +``` + +### Cookies + +You can read cookies using `this.cookie.get()` and `this.cookie.set()`. + +> When using `handleCookies` (in our example below) if it's used within a `useTask$` function that runs during the initial request, setting cookies won’t work as expected. This is because, during server-side rendering (SSR), the response is streamed, and HTTP requires all headers to be set before sending the first response. However, if handleCookies is used in useVisibleTask$, this issue doesn’t occur. If you need to set cookies for the initial document request you can use `plugin@.ts` or Middleware. + +```tsx +export const handleCookies = server$( + function () { + const userSession = this.cookie.get('user-session')?.value; + if (!userSession) { + this.cookie.set('user-session', 'new-session-id', { path: '/', httpOnly: true }); } + return userSession; } - return { - success: false, +); +``` + +### URL + +You can access the request URL and its components using `this.url`. + +```tsx +export const getRequestUrl = server$( + function () { + const requestUrl = this.url; + console.log('Request URL:', requestUrl); + return requestUrl; } -}) +); ``` -> Server$ can accept any number of arguments and return any value that can be serialized by Qwik, that includes primitives, objects, arrays, bigint, JSX nodes and even Promises, just to name a few. +### Headers +You can read headers using `this.headers.get()`. + +```tsx +export const getHeaders = server$( + function () { + const userAgent = this.headers.get('User-Agent'); + console.log('User-Agent:', userAgent); + return userAgent; + } +); +``` + +### Using Multiple RequestEvent Information + +Here's an example that combines environment variables, cookies, URL, and headers in a single function. + +```tsx +export const handleRequest = server$( + function () { + // Access environment variable + const dbKey = this.env.get('DB_KEY'); + + // Access cookies + const userSession = this.cookie.get('user-session')?.value; + if (!userSession) { + this.cookie.set('user-session', 'new-session-id', { path: '/', httpOnly: true }); + } + + // Access request URL + const requestUrl = this.url; + + // Access headers + const userAgent = this.headers.get('User-Agent'); + + console.log('Environment Variable:', dbKey); + console.log('User Session:', userSession); + console.log('Request URL:', requestUrl); + console.log('User-Agent:', userAgent); + + return { + dbKey, + userSession, + requestUrl, + userAgent + }; + } +); +``` ## Streaming Responses -`server$` can return a stream of data by using an async generator. This is useful for streaming data from the server to the client. +`server$` can return a stream of data by using an async generator function, which is useful for streaming data from the server to the client. -Terminating the generator on the client side (e.g. by calling `return()` on the generator, or by breaking out from your async for-loop) will terminate the connection. As with `AbortSignal` - -how the generator will terminate on the server side depends on the server runtime and how client disconnects are handled. +Terminating the generator on the client side (e.g., by calling `.return()` on the generator or by breaking out from your async for-of loop) will terminate the connection. Similar to `AbortSignal`, how the generator terminates on the server side depends on the server runtime and how client disconnects are handled. ```tsx import { component$, useSignal } from '@builder.io/qwik'; import { server$ } from '@builder.io/qwik-city'; -const stream = server$(async function* () { - // Creation of an array with 10 undefined values - const iterationRange = Array(10).fill().entries(); - - for (const [index] of iterationRange) { - // Yield returns the index during each iteration - yield index; - - // Waiting for 1 second before the next iteration - // This simulates a delay in the execution - await new Promise((resolve) => setTimeout(resolve, 1000)); +export const streamFromServer = server$( + // Async Generator Function + async function* () { + // Creation of an array with 10 undefined values + const iterationRange = Array(10).fill().entries(); + + for (const [value] of iterationRange) { + // Yield returns the array value during each iteration + yield value; + + // Waiting for 1 second before the next iteration + // This simulates a delay in the execution + await new Promise((resolve) => setTimeout(resolve, 1000)); + } } -}); +); + export default component$(() => { @@ -122,15 +203,18 @@ export default component$(() => { return (
    @@ -145,7 +229,7 @@ This API is actually used to implement QwikGPT streaming responses in our docs s ## How does `server$()` work? -A `server$()` wraps a function and returns an async proxy to the function. On the server, the proxy function directly calls the wrapped function, and a HTTP endpoint is automatically created by the `server$()` function. +`server$()` wraps a function and returns an async proxy to the function. On the server, the proxy function directly calls the wrapped function, and a HTTP endpoint is automatically created by the `server$()` function. On the client, the proxy function invokes the wrapped function via an HTTP request, using `fetch()`. @@ -165,3 +249,6 @@ When using `server$`, it's important to understand how [middleware functions](/d To ensure that a middleware function runs for both types of requests, it should be defined in the `plugin.ts` file. This ensures that the middleware is executed consistently for all incoming requests, regardless of whether they are normal page requests or `server$` requests. By [defining middleware in the `plugin.ts`](/docs/advanced/plugints) file, developers can maintain a centralized location for shared middleware logic, ensuring consistency and reducing potential errors or oversights. + + + diff --git a/packages/docs/src/routes/docs/integrations/image-optimization/index.mdx b/packages/docs/src/routes/docs/integrations/image-optimization/index.mdx index af8978d7be8..7d01d349b49 100644 --- a/packages/docs/src/routes/docs/integrations/image-optimization/index.mdx +++ b/packages/docs/src/routes/docs/integrations/image-optimization/index.mdx @@ -179,7 +179,6 @@ export default component$(() => { Unpic is a third-party image optimization library that works with existing image optimization CDNs. It provides an `Image` component that can be used to optimize images. - ```tsx {6} /Slot/ import { component$ } from '@builder.io/qwik'; import { Image } from '@unpic/qwik'; @@ -196,7 +195,6 @@ export default component$(() => { ); }); ``` - > **Note:** qwik-image and unpic are not a CDN and does not host your images. They work with existing image optimization CDNs. We suggest using some of the popular CDNs: > - Cloudinary diff --git a/packages/docs/src/routes/docs/integrations/modular-forms/index.mdx b/packages/docs/src/routes/docs/integrations/modular-forms/index.mdx index 3bf707a7320..34ea6b4d793 100644 --- a/packages/docs/src/routes/docs/integrations/modular-forms/index.mdx +++ b/packages/docs/src/routes/docs/integrations/modular-forms/index.mdx @@ -12,8 +12,6 @@ updated_at: '2023-10-03T18:53:59Z' created_at: '2023-04-28T22:00:03Z' --- -import CodeSandbox from '../../../../components/code-sandbox/index.tsx'; - # Modular Forms [Modular Forms](https://modularforms.dev/) is a type-safe form library built natively on Qwik. The headless design gives you full control over the visual appearance of your form. The library takes care of state management and input validation. @@ -175,7 +173,6 @@ export default component$(() => { If we now put all the building blocks together, we get a working login form. Below you can see the assembled code and try it out in the attached sandbox. - ```tsx // @ts-nocheck /* eslint-disable @typescript-eslint/no-unused-vars */ @@ -242,7 +239,6 @@ export default component$(() => { ); }); ``` - ## Summary diff --git a/packages/docs/src/routes/query/[id]/index.tsx b/packages/docs/src/routes/query/[id]/index.tsx index 8e8f9373880..732a2112d4c 100644 --- a/packages/docs/src/routes/query/[id]/index.tsx +++ b/packages/docs/src/routes/query/[id]/index.tsx @@ -1,134 +1,135 @@ import { component$ } from '@builder.io/qwik'; -import { routeLoader$, server$ } from '@builder.io/qwik-city'; -import { createClient } from '@supabase/supabase-js'; -import { normalizeLine, resolveContext } from '../../../components/qwik-gpt/search'; +// import { routeLoader$, server$ } from '@builder.io/qwik-city'; +// import { createClient } from '@supabase/supabase-js'; +// import { normalizeLine, resolveContext } from '../../../components/qwik-gpt/search'; -export const approveId = server$(async function (id: string, approved: boolean) { - const supabase = createClient(this.env.get('SUPABASE_URL')!, this.env.get('SUPABASE_KEY')!); - await supabase.from('search_queries').update({ approved }).filter('id', 'eq', id); -}); +// export const approveId = server$(async function (id: string, approved: boolean) { +// const supabase = createClient(this.env.get('SUPABASE_URL')!, this.env.get('SUPABASE_KEY')!); +// await supabase.from('search_queries').update({ approved }).filter('id', 'eq', id); +// }); -export const useQueryData = routeLoader$(async (ev) => { - if (ev.query.get('token') !== ev.env.get('DEBUG_TOKEN')) { - throw ev.redirect(308, '/'); - } - const query_id = ev.params.id; - const supabase = createClient(ev.env.get('SUPABASE_URL')!, ev.env.get('SUPABASE_KEY')!); - const output = await supabase - .from('search_queries') - .select('query, embedding, results, output, model, approved') - .filter('id', 'eq', query_id) - .limit(1); +// export const useQueryData = routeLoader$(async (ev) => { +// if (ev.query.get('token') !== ev.env.get('DEBUG_TOKEN')) { +// throw ev.redirect(308, '/'); +// } +// const query_id = ev.params.id; +// const supabase = createClient(ev.env.get('SUPABASE_URL')!, ev.env.get('SUPABASE_KEY')!); +// const output = await supabase +// .from('search_queries') +// .select('query, embedding, results, output, model, approved') +// .filter('id', 'eq', query_id) +// .limit(1); - if (!output.data || output.data.length !== 1) { - return null; - } - const entry = output.data[0]; +// if (!output.data || output.data.length !== 1) { +// return null; +// } +// const entry = output.data[0]; - const all_results = await supabase.rpc('match_docs_10', { - query_text: normalizeLine(entry.query).replaceAll(' ', '|'), - query_embedding: entry.embedding, - match_count: 40, - similarity_threshold: 0.6, - }); +// const all_results = await supabase.rpc('match_docs_10', { +// query_text: normalizeLine(entry.query).replaceAll(' ', '|'), +// query_embedding: entry.embedding, +// match_count: 40, +// similarity_threshold: 0.6, +// }); - all_results.data.forEach((result: any) => { - result.included = entry.results.some((r: any) => r.key === result.key); - }); +// all_results.data.forEach((result: any) => { +// result.included = entry.results.some((r: any) => r.key === result.key); +// }); - const output2 = await supabase.rpc('match_output_9', { - match_id: query_id, - }); - return { - id: query_id, - query: entry.query, - results: all_results, - input: await resolveContext(entry.results), - similar: output2.data, - output: entry.output, - model: entry.model, - approved: entry.approved, - }; -}); +// const output2 = await supabase.rpc('match_output_9', { +// match_id: query_id, +// }); +// return { +// id: query_id, +// query: entry.query, +// results: all_results, +// input: await resolveContext(entry.results), +// similar: output2.data, +// output: entry.output, +// model: entry.model, +// approved: entry.approved, +// }; +// }); export default component$(() => { - const queryData = useQueryData().value; + // const queryData = useQueryData().value; + const queryData = null; if (queryData === null) { return
    Query not found
    ; } - return ( -
    -

    Query: {queryData.query}

    -

    Model: {queryData.model}

    -
    -
    Currently {queryData.approved ? 'APPROVED' : 'NOT APPROVED'}
    -
    - -
    -
    -
    -
    -

    Input:

    -
    {queryData.input}
    -
    -
    -

    Output:

    -
    {queryData.output}
    -
    -
    + // return ( + //
    + //

    Query: {queryData.query}

    + //

    Model: {queryData.model}

    + //
    + //
    Currently {queryData.approved ? 'APPROVED' : 'NOT APPROVED'}
    + //
    + // + //
    + //
    + //
    + //
    + //

    Input:

    + //
    {queryData.input}
    + //
    + //
    + //

    Output:

    + //
    {queryData.output}
    + //
    + //
    -

    Results

    - - - {queryData.results.data.map((result: any, i: any) => ( - - - - - - - - ))} - -
    {i}{(result.similarity as number).toFixed(4)}{(result.fts_rank as number).toFixed(4)} - {result.file.replace('packages/docs/src/routes/', '')}:{result.line} - {result.content}
    + //

    Results

    + // + // + // {queryData.results.data.map((result: any, i: any) => ( + // + // + // + // + // + // + // + // ))} + // + //
    {i}{(result.similarity as number).toFixed(4)}{(result.fts_rank as number).toFixed(4)} + // {result.file.replace('packages/docs/src/routes/', '')}:{result.line} + // {result.content}
    -

    Similar queries

    - - - {queryData.similar.map((result: any, key: string) => ( - 0, - }} - > - - - - - - ))} - -
    {result.id}{result.query}{result.message}{result.rate}
    -
    - ); + //

    Similar queries

    + // + // + // {queryData.similar.map((result: any, key: string) => ( + // 0, + // }} + // > + // + // + // + // + // + // ))} + // + //
    {result.id}{result.query}{result.message}{result.rate}
    + //
    + // ); }); diff --git a/packages/docs/src/routes/query/index.tsx b/packages/docs/src/routes/query/index.tsx index da052a91700..7a6682ae15b 100644 --- a/packages/docs/src/routes/query/index.tsx +++ b/packages/docs/src/routes/query/index.tsx @@ -1,53 +1,54 @@ import { component$ } from '@builder.io/qwik'; -import { routeLoader$ } from '@builder.io/qwik-city'; -import { createClient } from '@supabase/supabase-js'; +// import { routeLoader$ } from '@builder.io/qwik-city'; +// import { createClient } from '@supabase/supabase-js'; -export const useQueryData = routeLoader$(async (ev) => { - const token = ev.query.get('token'); - if (token !== ev.env.get('DEBUG_TOKEN')) { - throw ev.redirect(308, '/'); - } - const supabase = createClient(ev.env.get('SUPABASE_URL')!, ev.env.get('SUPABASE_KEY')!); - const output = await supabase.rpc('list_queries_2', { - match_count: 100, - }); +// export const useQueryData = routeLoader$(async (ev) => { +// const token = ev.query.get('token'); +// if (token !== ev.env.get('DEBUG_TOKEN')) { +// throw ev.redirect(308, '/'); +// } +// const supabase = createClient(ev.env.get('SUPABASE_URL')!, ev.env.get('SUPABASE_KEY')!); +// const output = await supabase.rpc('list_queries_2', { +// match_count: 100, +// }); - return { - token, - results: output, - }; -}); +// return { +// token, +// results: output, +// }; +// }); export default component$(() => { - const queryData = useQueryData().value; + // const queryData = useQueryData().value; + const queryData = null; if (queryData === null) { return
    Query not found
    ; } - return ( -
    -

    Results

    - - - {queryData.results.data?.map((result: any, i: any) => ( - 0, - 'bg-blue-400': result.approved > 0, - }} - > - - - - - - ))} - -
    {i}{result.query}{result.created_at} - Open -
    -
    - ); + // return ( + //
    + //

    Results

    + // + // + // {queryData.results.data?.map((result: any, i: any) => ( + // 0, + // 'bg-blue-400': result.approved > 0, + // }} + // > + // + // + // + // + // + // ))} + // + //
    {i}{result.query}{result.created_at} + // Open + //
    + //
    + // ); }); diff --git a/packages/docs/tailwind.config.js b/packages/docs/tailwind.config.js index a2a6584fae2..0c2ce971e6b 100644 --- a/packages/docs/tailwind.config.js +++ b/packages/docs/tailwind.config.js @@ -1,7 +1,11 @@ module.exports = { content: ['./src/**/*.{js,ts,jsx,tsx}', './routes/**/*.{md,mdx}'], theme: { - extend: {}, + extend: { + colors: { + 'interactive-blue': '#009dfd', + }, + }, }, plugins: [], }; diff --git a/packages/docs/vite.config.mts b/packages/docs/vite.config.mts index 2833e1206a9..e3851b72c80 100644 --- a/packages/docs/vite.config.mts +++ b/packages/docs/vite.config.mts @@ -134,6 +134,9 @@ export default defineConfig(async () => { // Suppress errors like these: // FILE Module level directives cause errors when bundled, "use client" in FILE was ignored. return; + } else if (level == 'warn' && log.code === 'SOURCEMAP_ERROR') { + // https://github.com/vitejs/vite/issues/15012 + return; } defaultHandler(level, log); }, diff --git a/packages/eslint-plugin-qwik/package.json b/packages/eslint-plugin-qwik/package.json index 89eb597d917..5bb95f09d20 100644 --- a/packages/eslint-plugin-qwik/package.json +++ b/packages/eslint-plugin-qwik/package.json @@ -17,7 +17,7 @@ "redent": "^4.0.0" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "homepage": "https://github.com/QwikDev/qwik#readme", "keywords": [ diff --git a/packages/eslint-plugin-qwik/qwik.unit.ts b/packages/eslint-plugin-qwik/qwik.unit.ts index c411512174a..db139be62bc 100644 --- a/packages/eslint-plugin-qwik/qwik.unit.ts +++ b/packages/eslint-plugin-qwik/qwik.unit.ts @@ -5,7 +5,7 @@ import { RuleTester } from '@typescript-eslint/rule-tester'; import { fileURLToPath } from 'node:url'; import { rules } from './index'; import { readdir, readFile, stat } from 'node:fs/promises'; -import { join, dirname } from 'path'; +import { join } from 'path'; // https://typescript-eslint.io/packages/rule-tester/#vitest RuleTester.afterAll = vitest.afterAll; @@ -40,7 +40,7 @@ interface InvalidTestCase extends TestCase { } await (async function setupEsLintRuleTesters() { // list './test' directory content and set up one RuleTester per directory - const testDir = join(dirname(new URL(import.meta.url).pathname), './tests'); + const testDir = join(__dirname, './tests'); const ruleNames = await readdir(testDir); for (const ruleName of ruleNames) { const rule = rules[ruleName]; diff --git a/packages/insights/package.json b/packages/insights/package.json index 4dd7755b468..95d51542c0c 100644 --- a/packages/insights/package.json +++ b/packages/insights/package.json @@ -6,7 +6,7 @@ "@auth/core": "0.30.0", "@builder.io/qwik-auth": "workspace:^", "@libsql/client": "^0.5.6", - "@modular-forms/qwik": "^0.23.1", + "@modular-forms/qwik": "^0.24.0", "@typescript/analyze-trace": "^0.10.1", "density-clustering": "^1.3.0", "dotenv": "^16.4.5", @@ -22,7 +22,7 @@ "@netlify/edge-functions": "^2.3.1", "@types/density-clustering": "^1.3.3", "@types/eslint": "^8.56.10", - "@types/node": "^20.12.8", + "@types/node": "^20.14.1", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "autoprefixer": "^10.4.19", @@ -35,13 +35,12 @@ "prettier-plugin-tailwindcss": "^0.5.14", "tailwindcss": "^3.4.3", "typescript": "5.4.5", - "undici": "*", "vite": "^5.2.11", "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "private": true, "scripts": { diff --git a/packages/qwik-auth/.npmignore b/packages/qwik-auth/.npmignore new file mode 100644 index 00000000000..5ab56ceb7ac --- /dev/null +++ b/packages/qwik-auth/.npmignore @@ -0,0 +1,2 @@ +src +vite.config.ts \ No newline at end of file diff --git a/packages/qwik-auth/package.json b/packages/qwik-auth/package.json index 54983fb9f0a..42c0cf06cfb 100644 --- a/packages/qwik-auth/package.json +++ b/packages/qwik-auth/package.json @@ -1,7 +1,7 @@ { "name": "@builder.io/qwik-auth", "description": "Qwik Auth is powered by Auth.js, a battle tested library for authentication with 3rd party providers", - "version": "0.1.3", + "version": "0.2.2", "bugs": "https://github.com/QwikDev/qwik/issues", "dependencies": { "@auth/core": "0.30.0" @@ -14,7 +14,7 @@ "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "exports": { ".": { diff --git a/packages/qwik-auth/src/index.ts b/packages/qwik-auth/src/index.ts index c3d063bc19a..43177823ebc 100644 --- a/packages/qwik-auth/src/index.ts +++ b/packages/qwik-auth/src/index.ts @@ -1,14 +1,14 @@ +import type { AuthConfig } from '@auth/core'; import { Auth, skipCSRFCheck } from '@auth/core'; import type { AuthAction, Session } from '@auth/core/types'; -import type { AuthConfig } from '@auth/core'; import { implicit$FirstArg, type QRL } from '@builder.io/qwik'; import { globalAction$, routeLoader$, - type RequestEvent, - type RequestEventCommon, z, zod$, + type RequestEvent, + type RequestEventCommon, } from '@builder.io/qwik-city'; import { isServer } from '@builder.io/qwik/build'; import { parseString, splitCookiesString } from 'set-cookie-parser'; @@ -39,7 +39,7 @@ export function serverAuthQrl(authOptions: QRL<(ev: RequestEventCommon) => QwikA const isCredentials = providerId === 'credentials'; - const auth = await authOptions(req); + const auth = await patchAuthOptions(authOptions, req); const body = new URLSearchParams({ callbackUrl: callbackUrl as string }); Object.entries(rest).forEach(([key, value]) => { body.set(key, String(value)); @@ -80,7 +80,7 @@ export function serverAuthQrl(authOptions: QRL<(ev: RequestEventCommon) => QwikA const useAuthSignout = globalAction$( async ({ callbackUrl }, req) => { callbackUrl ??= defaultCallbackURL(req); - const auth = await authOptions(req); + const auth = await patchAuthOptions(authOptions, req); const body = new URLSearchParams({ callbackUrl }); await authAction(body, req, `/api/auth/signout`, auth); }, @@ -99,7 +99,7 @@ export function serverAuthQrl(authOptions: QRL<(ev: RequestEventCommon) => QwikA const action = req.url.pathname.slice(prefix.length + 1).split('/')[0] as AuthAction; - const auth = await authOptions(req); + const auth = await patchAuthOptions(authOptions, req); if (actions.includes(action) && req.url.pathname.startsWith(prefix + '/')) { // Casting to `Response` because, something is off with the types in `@auth/core` here: // Without passing `raw`, it should know it's supposed to return a `Response` object, but it doesn't. @@ -223,3 +223,11 @@ async function getSessionData(req: Request, options: AuthConfig): GetSessionResu throw new Error(data.message); } + +const patchAuthOptions = async ( + authOptions: QRL<(ev: RequestEventCommon) => QwikAuthConfig>, + req: RequestEventCommon +) => { + const options = await authOptions(req); + return { ...options, basePath: '/api/auth' }; +}; diff --git a/packages/qwik-city/buildtime/vite/dev-server.ts b/packages/qwik-city/buildtime/vite/dev-server.ts index 43e4620f381..2f9e9e68323 100644 --- a/packages/qwik-city/buildtime/vite/dev-server.ts +++ b/packages/qwik-city/buildtime/vite/dev-server.ts @@ -32,6 +32,7 @@ import { } from '../../middleware/request-handler/resolve-request-handlers'; import { formatError } from './format-error'; import { matchRoute } from '../../runtime/src/route-matcher'; +import type { QwikSerializer } from 'packages/qwik-city/middleware/request-handler/types'; export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { const matchRouteRequest = (pathname: string) => { @@ -215,9 +216,9 @@ export function ssrDevMiddleware(ctx: BuildContext, server: ViteDevServer) { version: '1', }; - const { _deserializeData, _serializeData, _verifySerializable } = + const { _deserialize, _serialize, _verifySerializable } = await server.ssrLoadModule('@qwik-serializer'); - const qwikSerializer = { _deserializeData, _serializeData, _verifySerializable }; + const qwikSerializer: QwikSerializer = { _deserialize, _serialize, _verifySerializable }; const { completion, requestEv } = runQwikCity( serverRequestEv, diff --git a/packages/qwik-city/buildtime/vite/plugin.ts b/packages/qwik-city/buildtime/vite/plugin.ts index 2dd8a9a5352..10e957278fb 100644 --- a/packages/qwik-city/buildtime/vite/plugin.ts +++ b/packages/qwik-city/buildtime/vite/plugin.ts @@ -13,7 +13,6 @@ import { build } from '../build'; import { ssrDevMiddleware, staticDistMiddleware } from './dev-server'; import { transformMenu } from '../markdown/menu'; import { generateQwikCityEntries } from '../runtime-generation/generate-entries'; -import { patchGlobalThis } from '../../middleware/node/node-fetch'; import type { QwikVitePlugin } from '@builder.io/qwik/optimizer'; import fs from 'node:fs'; import { @@ -42,9 +41,6 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { let ssrFormat: 'esm' | 'cjs' = 'esm'; let outDir: string | null = null; - // Patch Stream APIs - patchGlobalThis(); - globalThis.__qwikCityNew = true; const api: QwikCityPluginApi = { @@ -162,7 +158,7 @@ function qwikCityPlugin(userOpts?: QwikCityVitePluginOptions): any { const isSwRegister = id.endsWith(QWIK_CITY_SW_REGISTER); if (isSerializer) { - return `export {_deserializeData, _serializeData, _verifySerializable} from '@builder.io/qwik'`; + return `export {_deserialize, _serialize, _verifySerializable} from '@builder.io/qwik'`; } if (isCityPlan || isSwRegister) { if (!ctx.isDevServer && ctx.isDirty) { diff --git a/packages/qwik-city/middleware/azure-swa/index.ts b/packages/qwik-city/middleware/azure-swa/index.ts index 000cdfd4630..36d9a457e89 100644 --- a/packages/qwik-city/middleware/azure-swa/index.ts +++ b/packages/qwik-city/middleware/azure-swa/index.ts @@ -6,9 +6,10 @@ import type { ServerRequestEvent, } from '@builder.io/qwik-city/middleware/request-handler'; import { getNotFound } from '@qwik-city-not-found-paths'; -import { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; import { parseString } from 'set-cookie-parser'; import { isStaticPath } from '@qwik-city-static-paths'; +import type { QwikSerializer } from '../request-handler/types'; // @builder.io/qwik-city/middleware/azure-swa @@ -49,9 +50,9 @@ interface AzureCookie { /** @public */ export function createQwikCity(opts: QwikCityAzureOptions): AzureFunction { - const qwikSerializer = { - _deserializeData, - _serializeData, + const qwikSerializer: QwikSerializer = { + _deserialize, + _serialize, _verifySerializable, }; if (opts.manifest) { diff --git a/packages/qwik-city/middleware/bun/index.ts b/packages/qwik-city/middleware/bun/index.ts index 195f88f09f1..86f6fd50180 100644 --- a/packages/qwik-city/middleware/bun/index.ts +++ b/packages/qwik-city/middleware/bun/index.ts @@ -11,10 +11,11 @@ import { } from '@builder.io/qwik-city/middleware/request-handler'; import { getNotFound } from '@qwik-city-not-found-paths'; import { isStaticPath } from '@qwik-city-static-paths'; -import { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; import { setServerPlatform } from '@builder.io/qwik/server'; import { MIME_TYPES } from '../request-handler/mime-types'; import { join, extname } from 'node:path'; +import type { QwikSerializer } from '../request-handler/types'; /** @public */ export function createQwikCity(opts: QwikCityBunOptions) { @@ -22,9 +23,9 @@ export function createQwikCity(opts: QwikCityBunOptions) { // still missing from bun: last check was bun version 1.1.8 globalThis.TextEncoderStream ||= _TextEncoderStream_polyfill; - const qwikSerializer = { - _deserializeData, - _serializeData, + const qwikSerializer: QwikSerializer = { + _deserialize, + _serialize, _verifySerializable, }; if (opts.manifest) { diff --git a/packages/qwik-city/middleware/cloudflare-pages/index.ts b/packages/qwik-city/middleware/cloudflare-pages/index.ts index eee224c1e9b..86bf8c893c8 100644 --- a/packages/qwik-city/middleware/cloudflare-pages/index.ts +++ b/packages/qwik-city/middleware/cloudflare-pages/index.ts @@ -9,8 +9,9 @@ import { } from '@builder.io/qwik-city/middleware/request-handler'; import { getNotFound } from '@qwik-city-not-found-paths'; import { isStaticPath } from '@qwik-city-static-paths'; -import { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; import { setServerPlatform } from '@builder.io/qwik/server'; +import type { QwikSerializer } from '../request-handler/types'; // @builder.io/qwik-city/middleware/cloudflare-pages @@ -24,9 +25,9 @@ export function createQwikCity(opts: QwikCityCloudflarePagesOptions) { // @ts-ignore globalThis.TextEncoderStream = _TextEncoderStream_polyfill2; } - const qwikSerializer = { - _deserializeData, - _serializeData, + const qwikSerializer: QwikSerializer = { + _deserialize, + _serialize, _verifySerializable, }; if (opts.manifest) { diff --git a/packages/qwik-city/middleware/deno/index.ts b/packages/qwik-city/middleware/deno/index.ts index 4afd9cff5fd..2a0b26de999 100644 --- a/packages/qwik-city/middleware/deno/index.ts +++ b/packages/qwik-city/middleware/deno/index.ts @@ -9,11 +9,12 @@ import { } from '@builder.io/qwik-city/middleware/request-handler'; import { getNotFound } from '@qwik-city-not-found-paths'; import { isStaticPath } from '@qwik-city-static-paths'; -import { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; import { setServerPlatform } from '@builder.io/qwik/server'; import { MIME_TYPES } from '../request-handler/mime-types'; // @ts-ignore import { extname, fromFileUrl, join } from 'https://deno.land/std/path/mod.ts'; +import type { QwikSerializer } from '../request-handler/types'; // @builder.io/qwik-city/middleware/deno @@ -31,9 +32,9 @@ export interface ServeHandlerInfo { /** @public */ export function createQwikCity(opts: QwikCityDenoOptions) { - const qwikSerializer = { - _deserializeData, - _serializeData, + const qwikSerializer: QwikSerializer = { + _deserialize, + _serialize, _verifySerializable, }; if (opts.manifest) { diff --git a/packages/qwik-city/middleware/netlify-edge/index.ts b/packages/qwik-city/middleware/netlify-edge/index.ts index 703cb6af86e..cb035615045 100644 --- a/packages/qwik-city/middleware/netlify-edge/index.ts +++ b/packages/qwik-city/middleware/netlify-edge/index.ts @@ -10,17 +10,18 @@ import { } from '@builder.io/qwik-city/middleware/request-handler'; import { getNotFound } from '@qwik-city-not-found-paths'; import { isStaticPath } from '@qwik-city-static-paths'; -import { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; import { setServerPlatform } from '@builder.io/qwik/server'; +import type { QwikSerializer } from '../request-handler/types'; // @builder.io/qwik-city/middleware/netlify-edge declare const Deno: any; /** @public */ export function createQwikCity(opts: QwikCityNetlifyOptions) { - const qwikSerializer = { - _deserializeData, - _serializeData, + const qwikSerializer: QwikSerializer = { + _deserialize, + _serialize, _verifySerializable, }; if (opts.manifest) { diff --git a/packages/qwik-city/middleware/node/index.ts b/packages/qwik-city/middleware/node/index.ts index 1e53e714d31..f052df872db 100644 --- a/packages/qwik-city/middleware/node/index.ts +++ b/packages/qwik-city/middleware/node/index.ts @@ -12,20 +12,17 @@ import { extname, join, basename } from 'node:path'; import { fileURLToPath } from 'node:url'; import { computeOrigin, fromNodeHttp, getUrl } from './http'; import { MIME_TYPES } from '../request-handler/mime-types'; -import { patchGlobalThis } from './node-fetch'; -import { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; import type { Http2ServerRequest } from 'node:http2'; +import type { QwikSerializer } from '../request-handler/types'; // @builder.io/qwik-city/middleware/node /** @public */ export function createQwikCity(opts: QwikCityNodeRequestOptions) { - // Patch Stream APIs - patchGlobalThis(); - - const qwikSerializer = { - _deserializeData, - _serializeData, + const qwikSerializer: QwikSerializer = { + _deserialize, + _serialize, _verifySerializable, }; if (opts.manifest) { diff --git a/packages/qwik-city/middleware/node/node-fetch.ts b/packages/qwik-city/middleware/node/node-fetch.ts deleted file mode 100644 index 328a1b6002e..00000000000 --- a/packages/qwik-city/middleware/node/node-fetch.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - TextEncoderStream, - TextDecoderStream, - WritableStream, - ReadableStream, -} from 'node:stream/web'; -import { fetch, Headers, Request, Response, FormData } from 'undici'; - -import crypto from 'crypto'; - -// TODO: remove when undici is removed -export function patchGlobalThis() { - if ( - typeof global !== 'undefined' && - typeof globalThis.fetch !== 'function' && - typeof process !== 'undefined' && - process.versions.node - ) { - globalThis.fetch = fetch as any; - globalThis.Headers = Headers as any; - globalThis.Request = Request as any; - globalThis.Response = Response as any; - globalThis.FormData = FormData as any; - } - if (typeof globalThis.TextEncoderStream === 'undefined') { - // @ts-ignore - globalThis.TextEncoderStream = TextEncoderStream; - // @ts-ignore - globalThis.TextDecoderStream = TextDecoderStream; - } - if (typeof globalThis.WritableStream === 'undefined') { - globalThis.WritableStream = WritableStream as any; - globalThis.ReadableStream = ReadableStream as any; - } - if (typeof globalThis.crypto === 'undefined') { - globalThis.crypto = crypto.webcrypto as any; - } -} diff --git a/packages/qwik-city/middleware/request-handler/api.md b/packages/qwik-city/middleware/request-handler/api.md index e130f662b74..26177854d0d 100644 --- a/packages/qwik-city/middleware/request-handler/api.md +++ b/packages/qwik-city/middleware/request-handler/api.md @@ -5,7 +5,7 @@ ```ts import type { Action } from '@builder.io/qwik-city'; -import type { _deserializeData } from '@builder.io/qwik'; +import type { _deserialize } from '@builder.io/qwik'; import type { EnvGetter as EnvGetter_2 } from '@builder.io/qwik-city/middleware/request-handler'; import type { FailReturn } from '@builder.io/qwik-city'; import type { Loader as Loader_2 } from '@builder.io/qwik-city'; @@ -16,7 +16,7 @@ import type { RenderOptions } from '@builder.io/qwik/server'; import type { RequestEvent as RequestEvent_2 } from '@builder.io/qwik-city'; import type { RequestHandler as RequestHandler_2 } from '@builder.io/qwik-city/middleware/request-handler'; import type { ResolveSyncValue as ResolveSyncValue_2 } from '@builder.io/qwik-city/middleware/request-handler'; -import type { _serializeData } from '@builder.io/qwik'; +import type { _serialize } from '@builder.io/qwik'; import type { ValueOrPromise } from '@builder.io/qwik'; import type { _verifySerializable } from '@builder.io/qwik'; diff --git a/packages/qwik-city/middleware/request-handler/request-event.ts b/packages/qwik-city/middleware/request-handler/request-event.ts index 506387b5530..66d56857125 100644 --- a/packages/qwik-city/middleware/request-handler/request-event.ts +++ b/packages/qwik-city/middleware/request-handler/request-event.ts @@ -331,13 +331,13 @@ const parseRequest = async ( const data = query.get(QDATA_KEY); if (data) { try { - return qwikSerializer._deserializeData(decodeURIComponent(data)); + return qwikSerializer._deserialize(decodeURIComponent(data)) as JSONValue | undefined; } catch (err) { // } } } - return qwikSerializer._deserializeData(await request.text()); + return qwikSerializer._deserialize(await request.text()) as JSONValue | undefined; } return undefined; }; diff --git a/packages/qwik-city/middleware/request-handler/resolve-request-handlers.ts b/packages/qwik-city/middleware/request-handler/resolve-request-handlers.ts index d44edd4d484..ed4fbcf81b4 100644 --- a/packages/qwik-city/middleware/request-handler/resolve-request-handlers.ts +++ b/packages/qwik-city/middleware/request-handler/resolve-request-handlers.ts @@ -23,7 +23,7 @@ import { QACTION_KEY, QFN_KEY } from '../../runtime/src/constants'; import { IsQData, QDATA_JSON } from './user-response'; import { HttpStatus } from './http-status-codes'; import type { Render, RenderToStringResult } from '@builder.io/qwik/server'; -import type { QRL, _deserializeData, _serializeData } from '@builder.io/qwik'; +import type { QRL, _deserialize, _serialize } from '@builder.io/qwik'; import { getQwikCityServerData } from './response-page'; import { RedirectMessage } from './redirect-handler'; import { ServerError } from './error-handler'; @@ -311,11 +311,11 @@ async function pureServerFunction(ev: RequestEvent) { } catch (err) { if (err instanceof ServerError) { ev.headers.set('Content-Type', 'application/qwik-json'); - ev.send(err.status, await qwikSerializer._serializeData(err.data, true)); + ev.send(err.status, await qwikSerializer._serialize([err.data])); return; } ev.headers.set('Content-Type', 'application/qwik-json'); - ev.send(500, await qwikSerializer._serializeData(err, true)); + ev.send(500, await qwikSerializer._serialize([err])); return; } if (isAsyncIterator(result)) { @@ -326,7 +326,7 @@ async function pureServerFunction(ev: RequestEvent) { if (isDev) { verifySerializable(qwikSerializer, item, qrl); } - const message = await qwikSerializer._serializeData(item, true); + const message = await qwikSerializer._serialize([item]); if (ev.signal.aborted) { break; } @@ -336,7 +336,7 @@ async function pureServerFunction(ev: RequestEvent) { } else { verifySerializable(qwikSerializer, result, qrl); ev.headers.set('Content-Type', 'application/qwik-json'); - const message = await qwikSerializer._serializeData(result, true); + const message = await qwikSerializer._serialize([result]); ev.send(200, message); } return; @@ -542,7 +542,7 @@ export async function renderQData(requestEv: RequestEvent) { const writer = requestEv.getWritableStream().getWriter(); const qwikSerializer = (requestEv as RequestEventInternal)[RequestEvQwikSerializer]; // write just the page json data to the response body - const data = await qwikSerializer._serializeData(qData, true); + const data = await qwikSerializer._serialize([qData]); writer.write(encoder.encode(data)); requestEv.sharedMap.set('qData', qData); diff --git a/packages/qwik-city/middleware/request-handler/types.ts b/packages/qwik-city/middleware/request-handler/types.ts index e8faa4894c5..c09064ee41d 100644 --- a/packages/qwik-city/middleware/request-handler/types.ts +++ b/packages/qwik-city/middleware/request-handler/types.ts @@ -3,7 +3,7 @@ import type { QwikCityPlan, FailReturn, Action, Loader } from '@builder.io/qwik- import type { ErrorResponse } from './error-handler'; import type { AbortMessage, RedirectMessage } from './redirect-handler'; import type { RequestEventInternal } from './request-event'; -import type { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import type { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; /** @public */ export interface EnvGetter { @@ -563,8 +563,8 @@ export interface CookieValue { /** @public */ export interface QwikSerializer { - _deserializeData: typeof _deserializeData; - _serializeData: typeof _serializeData; + _deserialize: typeof _deserialize; + _serialize: typeof _serialize; _verifySerializable: typeof _verifySerializable; } diff --git a/packages/qwik-city/middleware/vercel-edge/index.ts b/packages/qwik-city/middleware/vercel-edge/index.ts index 088010e0087..6378fa54fb8 100644 --- a/packages/qwik-city/middleware/vercel-edge/index.ts +++ b/packages/qwik-city/middleware/vercel-edge/index.ts @@ -8,8 +8,9 @@ import { } from '@builder.io/qwik-city/middleware/request-handler'; import { getNotFound } from '@qwik-city-not-found-paths'; import { isStaticPath } from '@qwik-city-static-paths'; -import { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; import { setServerPlatform } from '@builder.io/qwik/server'; +import type { QwikSerializer } from '../request-handler/types'; // @builder.io/qwik-city/middleware/vercel-edge const COUNTRY_HEADER_NAME = 'x-vercel-ip-country'; @@ -21,9 +22,9 @@ const BASE_URL = 'BASE_URL'; /** @public */ export function createQwikCity(opts: QwikCityVercelEdgeOptions) { - const qwikSerializer = { - _deserializeData, - _serializeData, + const qwikSerializer: QwikSerializer = { + _deserialize, + _serialize, _verifySerializable, }; if (opts.manifest) { diff --git a/packages/qwik-city/package.json b/packages/qwik-city/package.json index 52f17add584..e0090dfd79c 100644 --- a/packages/qwik-city/package.json +++ b/packages/qwik-city/package.json @@ -8,19 +8,18 @@ "@types/mdx": "^2.0.13", "source-map": "0.7.4", "svgo": "^3.2.0", - "undici": "*", "vfile": "^6.0.1", "vite": "^5.2.11", "vite-imagetools": "^6.2.9", - "zod": "^3.23.6" + "zod": "^3.23.8" }, "devDependencies": { "@azure/functions": "^3.5.1", "@builder.io/qwik": "workspace:^", - "@microsoft/api-extractor": "^7.43.1", + "@microsoft/api-extractor": "7.43.1", "@netlify/edge-functions": "^2.3.1", "@types/mdast": "^4.0.1", - "@types/node": "^20.12.8", + "@types/node": "^20.14.1", "@types/refractor": "^3.4.1", "@types/set-cookie-parser": "^2.4.7", "estree-util-value-to-estree": "3.1.1", @@ -43,7 +42,7 @@ "yaml": "^2.4.2" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "exports": { ".": { diff --git a/packages/qwik-city/runtime/src/api.md b/packages/qwik-city/runtime/src/api.md index 01ecfe839aa..5e6aead8997 100644 --- a/packages/qwik-city/runtime/src/api.md +++ b/packages/qwik-city/runtime/src/api.md @@ -415,7 +415,7 @@ export const RouterOutlet: Component; // Warning: (ae-forgotten-export) The symbol "ServerConfig" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const server$: (first: T, options?: ServerConfig | undefined) => ServerQRL; +export const server$: (qrl: T, options?: ServerConfig | undefined) => ServerQRL; // @public (undocumented) export type ServerFunction = { diff --git a/packages/qwik-city/runtime/src/form-component.tsx b/packages/qwik-city/runtime/src/form-component.tsx index c393f287619..0da3f4a604f 100644 --- a/packages/qwik-city/runtime/src/form-component.tsx +++ b/packages/qwik-city/runtime/src/form-component.tsx @@ -1,10 +1,10 @@ import { jsx, - type QwikJSX, component$, Slot, - type QRLEventHandlerMulti, $, + type QwikJSX, + type QRLEventHandlerMulti, } from '@builder.io/qwik'; import type { ActionStore } from './types'; import { useNavigate } from './use-functions'; @@ -85,10 +85,10 @@ export const Form = ( action: action.actionPath, 'preventdefault:submit': !reloadDocument, onSubmit$: [ + // Since v2, this fires before the action is executed so it can be prevented + onSubmit$, // action.submit "submitcompleted" event for onSubmitCompleted$ events !reloadDocument ? action.submit : undefined, - // TODO: v2 breaking change this should fire before the action.submit - onSubmit$, ], method: 'post', ['data-spa-reset']: spaReset ? 'true' : undefined, diff --git a/packages/qwik-city/runtime/src/server-functions.ts b/packages/qwik-city/runtime/src/server-functions.ts index 2056290b407..f647a6d48b2 100644 --- a/packages/qwik-city/runtime/src/server-functions.ts +++ b/packages/qwik-city/runtime/src/server-functions.ts @@ -6,8 +6,8 @@ import { useContext, type ValueOrPromise, useStore, - _serializeData, - _deserializeData, + _serialize, + _deserialize, _getContextElement, _getContextEvent, _wrapProp, @@ -243,7 +243,10 @@ export const zodQrl = (( return z.object(obj); } }); - const data = inputData ?? (await ev.parseBody()); + let data = inputData; + if (!data) { + data = await ev.parseBody(); + } const result = await (await schema).safeParseAsync(data); if (result.success) { return result; @@ -273,7 +276,8 @@ export const zod$ = /*#__PURE__*/ implicit$FirstArg(zodQrl) as ZodConstructor; const deepFreeze = (obj: any) => { Object.getOwnPropertyNames(obj).forEach((prop) => { const value = obj[prop]; - if (value && typeof value === 'object') { + // we assume that a frozen object is a circular reference and fully deep frozen + if (value && typeof value === 'object' && !Object.isFrozen(value)) { deepFreeze(value); } }); @@ -347,7 +351,7 @@ export const serverQrl = ( }, signal, }; - const body = await _serializeData([qrl, ...filtered], false); + const body = await _serialize([qrl, ...filtered]); if (method === 'GET') { query += `&${QDATA_KEY}=${encodeURIComponent(body)}`; } else { @@ -375,7 +379,7 @@ export const serverQrl = ( })(); } else if (contentType === 'application/qwik-json') { const str = await res.text(); - const obj = await _deserializeData(str, ctxElm ?? document.documentElement); + const [obj] = _deserialize(str, ctxElm ?? document.documentElement); if (res.status === 500) { throw obj; } @@ -455,7 +459,8 @@ const deserializeStream = async function* ( const lines = buffer.split(/\n/); buffer = lines.pop()!; for (const line of lines) { - yield await _deserializeData(line, ctxElm); + const [deserializedData] = _deserialize(line, ctxElm); + yield deserializedData; } } } finally { diff --git a/packages/qwik-city/runtime/src/service-worker/utils.unit.ts b/packages/qwik-city/runtime/src/service-worker/utils.unit.ts index 5e2b14000ee..565cf9c0337 100644 --- a/packages/qwik-city/runtime/src/service-worker/utils.unit.ts +++ b/packages/qwik-city/runtime/src/service-worker/utils.unit.ts @@ -1,4 +1,3 @@ -import { Request as NodeRequest, Response as NodeResponse } from 'undici'; import type { AppBundle } from './types'; import { getCacheToDelete, isAppBundleRequest, useCache } from './utils'; import { assert, test } from 'vitest'; @@ -75,9 +74,9 @@ test('useCache', () => { export function mockRequest(url?: string): Request { url = url || 'https://qwik.dev/'; - return new NodeRequest(url) as any; + return new Request(url); } export function mockResponse(body?: any): Response { - return new NodeResponse(body) as any; + return new Response(body); } diff --git a/packages/qwik-city/runtime/src/use-endpoint.ts b/packages/qwik-city/runtime/src/use-endpoint.ts index 996d5275045..67746fd71d1 100644 --- a/packages/qwik-city/runtime/src/use-endpoint.ts +++ b/packages/qwik-city/runtime/src/use-endpoint.ts @@ -1,7 +1,7 @@ import { getClientDataPath } from './utils'; import { CLIENT_DATA_CACHE } from './constants'; import type { ClientPageData, RouteActionValue } from './types'; -import { _deserializeData } from '@builder.io/qwik'; +import { _deserialize } from '@builder.io/qwik'; import { prefetchSymbols } from './client-navigate'; export const loadClientData = async ( @@ -42,7 +42,7 @@ export const loadClientData = async ( if ((rsp.headers.get('content-type') || '').includes('json')) { // we are safe we are reading a q-data.json return rsp.text().then((text) => { - const clientData = _deserializeData(text, element) as ClientPageData | null; + const [clientData] = _deserialize(text, element) as [ClientPageData]; if (!clientData) { location.href = url.href; return; diff --git a/packages/qwik-city/static/node/index.ts b/packages/qwik-city/static/node/index.ts index 337cf468980..4f31e3ab603 100644 --- a/packages/qwik-city/static/node/index.ts +++ b/packages/qwik-city/static/node/index.ts @@ -3,7 +3,6 @@ import { createSystem } from './node-system'; import { isMainThread, workerData } from 'node:worker_threads'; import { mainThread } from '../main-thread'; import { workerThread } from '../worker-thread'; -import { patchGlobalThis } from 'packages/qwik-city/middleware/node/node-fetch'; export async function generate(opts: StaticGenerateOptions) { if (isMainThread) { @@ -17,8 +16,6 @@ export async function generate(opts: StaticGenerateOptions) { if (!isMainThread && workerData) { (async () => { - patchGlobalThis(); - // self initializing worker thread with workerData const sys = await createSystem(workerData); await workerThread(sys); diff --git a/packages/qwik-city/static/node/node-system.ts b/packages/qwik-city/static/node/node-system.ts index 14904b15f54..1a49d3b11c6 100644 --- a/packages/qwik-city/static/node/node-system.ts +++ b/packages/qwik-city/static/node/node-system.ts @@ -2,15 +2,12 @@ import type { StaticGenerateOptions, System } from '../types'; import fs from 'node:fs'; import { dirname, join } from 'node:path'; -import { patchGlobalThis } from '../../middleware/node/node-fetch'; import { createNodeMainProcess } from './node-main'; import { createNodeWorkerProcess } from './node-worker'; import { normalizePath } from '../../utils/fs'; /** @public */ export async function createSystem(opts: StaticGenerateOptions) { - patchGlobalThis(); - const createWriteStream = (filePath: string) => { return fs.createWriteStream(filePath, { flags: 'w', diff --git a/packages/qwik-city/static/worker-thread.ts b/packages/qwik-city/static/worker-thread.ts index 962631cb683..d5b3cd66398 100644 --- a/packages/qwik-city/static/worker-thread.ts +++ b/packages/qwik-city/static/worker-thread.ts @@ -10,7 +10,8 @@ import type { ServerRequestEvent } from '@builder.io/qwik-city/middleware/reques import { requestHandler } from '@builder.io/qwik-city/middleware/request-handler'; import { pathToFileURL } from 'node:url'; import { WritableStream } from 'node:stream/web'; -import { _deserializeData, _serializeData, _verifySerializable } from '@builder.io/qwik'; +import { _deserialize, _serialize, _verifySerializable } from '@builder.io/qwik'; +import type { QwikSerializer } from '../middleware/request-handler/types'; export async function workerThread(sys: System) { const ssgOpts = sys.getOptions(); @@ -63,9 +64,9 @@ async function workerRender( pendingPromises: Set>, callback: (result: StaticWorkerRenderResult) => void ) { - const qwikSerializer = { - _deserializeData, - _serializeData, + const qwikSerializer: QwikSerializer = { + _deserialize, + _serialize, _verifySerializable, }; // pathname and origin already normalized at this point @@ -188,7 +189,7 @@ async function workerRender( }; }); - const serialized = await _serializeData(qData, true); + const serialized = await _serialize([qData]); dataWriter.write(serialized); writePromises.push( diff --git a/packages/qwik-labs/package.json b/packages/qwik-labs/package.json index de8be98620d..70dc3a71f55 100644 --- a/packages/qwik-labs/package.json +++ b/packages/qwik-labs/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@builder.io/qwik": "workspace:^", "@types/eslint": "^8.56.10", - "@types/node": "^20.12.8", + "@types/node": "^20.14.1", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "eslint": "^8.57.0", @@ -13,11 +13,10 @@ "np": "^10.0.5", "prettier": "^3.2.5", "typescript": "5.4.5", - "undici": "*", "vite": "^5.2.11" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "exports": { ".": { diff --git a/packages/qwik-react/package.json b/packages/qwik-react/package.json index 79716d7feee..0853b5f3164 100644 --- a/packages/qwik-react/package.json +++ b/packages/qwik-react/package.json @@ -1,19 +1,19 @@ { "name": "@builder.io/qwik-react", "description": "QwikReact allows adding React components into existing Qwik application", - "version": "0.5.4", + "version": "0.5.5", "bugs": "https://github.com/QwikDev/qwik/issues", "devDependencies": { "@builder.io/qwik": "workspace:^", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.0", - "react": "18.3.1", - "react-dom": "18.3.1", + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "react": "18.2.0", + "react-dom": "18.2.0", "typescript": "5.4.5", - "vite": "^5.2.11" + "vite": "^5.2.10" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "exports": { ".": { @@ -36,10 +36,10 @@ "main": "./lib/index.qwik.mjs", "peerDependencies": { "@builder.io/qwik": "workspace:^", - "@types/react": "^18.3.1", - "@types/react-dom": "^18.3.0", - "react": "18.3.1", - "react-dom": "18.3.1" + "@types/react": "^18.2.79", + "@types/react-dom": "^18.2.25", + "react": "18.2.0", + "react-dom": "18.2.0" }, "qwik": "./lib/index.qwik.mjs", "repository": { diff --git a/packages/qwik-react/src/react/qwikify.tsx b/packages/qwik-react/src/react/qwikify.tsx index 9f5457e171c..aa3f161cd89 100644 --- a/packages/qwik-react/src/react/qwikify.tsx +++ b/packages/qwik-react/src/react/qwikify.tsx @@ -10,6 +10,7 @@ import { Slot, RenderOnce, useStylesScoped$, + useStore, } from '@builder.io/qwik'; import { isBrowser, isServer } from '@builder.io/qwik/build'; @@ -32,7 +33,7 @@ export function qwikifyQrl>( const slotRef = useSignal(); const internalState = useSignal>>(); const [signal, isClientOnly] = useWakeupSignal(props, opts); - const hydrationKeys = {}; + const hydrationKeys = useStore({}); const TagName = opts?.tagName ?? ('qwik-react' as any); // Task takes cares of updates and partial hydration @@ -92,7 +93,7 @@ export function qwikifyQrl>( } return ( - + <> { @@ -114,7 +115,7 @@ export function qwikifyQrl>( - + ); }); } diff --git a/packages/qwik-react/src/react/server-render.tsx b/packages/qwik-react/src/react/server-render.tsx index e66cd0aeb82..523f46bdee0 100644 --- a/packages/qwik-react/src/react/server-render.tsx +++ b/packages/qwik-react/src/react/server-render.tsx @@ -1,4 +1,4 @@ -import { type QRL, type Signal, Slot, SSRRaw, SSRStream } from '@builder.io/qwik'; +import { type QRL, type Signal, Slot, SSRComment, SSRRaw, SSRStream } from '@builder.io/qwik'; import { getHostProps, mainExactProps, getReactProps } from './slot'; import { renderToString } from 'react-dom/server'; import { isServer } from '@builder.io/qwik/build'; @@ -26,13 +26,17 @@ export async function renderFromServer( {async function* () { + yield ; yield ; + yield ; yield ( ); + yield ; yield ; + yield ; }} @@ -41,7 +45,9 @@ export async function renderFromServer( return ( <> + + diff --git a/packages/qwik-react/src/vite.ts b/packages/qwik-react/src/vite.ts index 62f1b9629a4..0acf3cfe849 100644 --- a/packages/qwik-react/src/vite.ts +++ b/packages/qwik-react/src/vite.ts @@ -3,6 +3,7 @@ export function qwikReact(): any { 'react', 'react-dom', 'react-dom/client', + 'react-dom/server', 'react/jsx-runtime', 'react/jsx-dev-runtime', ]; diff --git a/packages/qwik-worker/package.json b/packages/qwik-worker/package.json index 0d8b0eef1a1..cbe6f502e96 100644 --- a/packages/qwik-worker/package.json +++ b/packages/qwik-worker/package.json @@ -9,7 +9,7 @@ "vite-plugin-static-copy": "^1.0.4" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "exports": { ".": { diff --git a/packages/qwik-worker/src/index.ts b/packages/qwik-worker/src/index.ts index ab8da47d10e..ab242a8c43c 100644 --- a/packages/qwik-worker/src/index.ts +++ b/packages/qwik-worker/src/index.ts @@ -1,10 +1,4 @@ -import { - $, - implicit$FirstArg, - type QRL, - _getContextElement, - _serializeData, -} from '@builder.io/qwik'; +import { $, implicit$FirstArg, type QRL, _getContextElement, _serialize } from '@builder.io/qwik'; //@ts-ignore import workerUrl from './worker.js?worker&url'; @@ -57,7 +51,7 @@ export const workerQrl: WorkerConstructorQRL = (qrl) => { } return arg; }); - const data = await _serializeData([qrl, ...filtered], false); + const data = await _serialize([qrl, ...filtered]); return new Promise((resolve, reject) => { const handler = ({ data }: MessageEvent) => { if (Array.isArray(data) && data.length === 3 && data[0] === requestId) { diff --git a/packages/qwik/package.json b/packages/qwik/package.json index 415f29a8655..fa0e177da2b 100644 --- a/packages/qwik/package.json +++ b/packages/qwik/package.json @@ -33,7 +33,7 @@ "kleur": "4.1.5" }, "engines": { - "node": ">=16.8.0 <18.0.0 || >=18.11" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "exports": { ".": { diff --git a/packages/qwik/src/cli/utils/utils.ts b/packages/qwik/src/cli/utils/utils.ts index 020c64840c7..8cd032a7a47 100644 --- a/packages/qwik/src/cli/utils/utils.ts +++ b/packages/qwik/src/cli/utils/utils.ts @@ -81,7 +81,7 @@ export function cleanPackageJson(srcPkg: IntegrationPackageJson) { types: srcPkg.types, exports: srcPkg.exports, files: srcPkg.files, - engines: { node: '^18.17.0 || ^20.3.0 || >=21.0.0' }, + engines: { node: srcPkg.engines?.node || '^18.17.0 || ^20.3.0 || >=21.0.0' }, }; Object.keys(cleanedPkg).forEach((prop) => { diff --git a/packages/qwik/src/core/api.md b/packages/qwik/src/core/api.md index e8df737089e..27eb59e2551 100644 --- a/packages/qwik/src/core/api.md +++ b/packages/qwik/src/core/api.md @@ -162,6 +162,11 @@ export interface ComponentBaseProps { // @public export const componentQrl: >(componentQrl: QRL>) => Component; +// @public (undocumented) +export interface ComputedSignal2 extends ReadonlySignal2 { + force(): void; +} + // @internal (undocumented) export const _CONST_PROPS: unique symbol; @@ -197,9 +202,21 @@ export interface CorrectedToggleEvent extends Event { readonly prevState: 'open' | 'closed'; } +// @public (undocumented) +export const createComputed2$: (first: () => T) => ComputedSignal2; + +// @public (undocumented) +export const createComputed2Qrl: (qrl: QRL<() => T>) => ComputedSignal2; + // @public export const createContextId: (name: string) => ContextId; +// @public (undocumented) +export const createSignal2: { + (): Signal2; + (value: T): Signal2; +}; + // @public (undocumented) export interface CSSProperties extends CSS_2.Properties, CSS_2.PropertiesHyphen { [v: `--${string}`]: string | number | undefined; @@ -213,6 +230,9 @@ export interface DataHTMLAttributes extends Attrs<'data', T> export interface DelHTMLAttributes extends Attrs<'del', T> { } +// @internal +export function _deserialize(rawStateData: string | null, element?: unknown): unknown[]; + // @internal (undocumented) export const _deserializeData: (data: string, element?: unknown) => any; @@ -340,6 +360,9 @@ string | undefined, export interface EmbedHTMLAttributes extends Attrs<'embed', T> { } +// @internal (undocumented) +export const _EMPTY_ARRAY: any[]; + // @public (undocumented) export interface ErrorBoundaryStore { // (undocumented) @@ -347,7 +370,7 @@ export interface ErrorBoundaryStore { } // @public (undocumented) -export const event$: (first: T) => QRL; +export const event$: (qrl: T) => QRL; // @public export type EventHandler = { @@ -457,7 +480,7 @@ export interface ImgHTMLAttributes extends Attrs<'img', T> { } // @public -export const implicit$FirstArg: (fn: (first: QRL, ...rest: REST) => RET) => (first: FIRST, ...rest: REST) => RET; +export const implicit$FirstArg: (fn: (qrl: QRL, ...rest: REST) => RET) => ((qrl: FIRST, ...rest: REST) => RET); // Warning: (ae-internal-missing-underscore) The name "inlinedQrl" should be prefixed with an underscore because the declaration is marked as @internal // @@ -507,10 +530,8 @@ export type IntrinsicSVGElements = { // @internal (undocumented) export const _isJSXNode: (n: unknown) => n is JSXNode; -// Warning: (ae-forgotten-export) The symbol "Signal2_2" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -export const isSignal: (value: any) => value is Signal2_2; +export const isSignal: (value: any) => value is Signal2; // @internal (undocumented) export function _isStringifiable(value: unknown): value is _Stringifiable; @@ -916,6 +937,14 @@ export type QwikWheelEvent = NativeWheelEvent; // @public (undocumented) export type ReadonlySignal = Readonly>; +// @public (undocumented) +export interface ReadonlySignal2 { + // (undocumented) + readonly untrackedValue: T; + // (undocumented) + readonly value: T; +} + // @internal (undocumented) export const _regSymbol: (symbol: any, hash: string) => any; @@ -1036,6 +1065,9 @@ export interface ScriptHTMLAttributes extends Attrs<'script', export interface SelectHTMLAttributes extends Attrs<'select', T> { } +// @internal +export function _serialize(data: unknown[]): Promise; + // @internal (undocumented) export const _serializeData: (data: any, pureQRL?: boolean) => Promise; @@ -1095,12 +1127,20 @@ export abstract class _SharedContainer implements Container2 { trackSignalValue(signal: Signal, subscriber: Effect, property: string): T; } -// @public (undocumented) +// @public export interface Signal { // (undocumented) value: T; } +// @public (undocumented) +export interface Signal2 extends ReadonlySignal2 { + // (undocumented) + untrackedValue: T; + // (undocumented) + value: T; +} + // @public (undocumented) export type Size = number | string; @@ -1848,6 +1888,9 @@ export const useComputed$: Computed; // @public (undocumented) export const useComputedQrl: ComputedQRL; +// @public @deprecated +export const useConstant: (value: (() => T) | T) => T; + // Warning: (ae-forgotten-export) The symbol "UseContext" needs to be exported by the entry point index.d.ts // // @public @@ -1893,12 +1936,12 @@ export function useServerData(key: string, defaultValue: B): T | B; // @public (undocumented) export interface UseSignal { // (undocumented) - (): Signal; + (): Signal2; // (undocumented) - (value: T | (() => T)): Signal; + (value: T | (() => T)): Signal2; } -// @public (undocumented) +// @public export const useSignal: UseSignal; // @public @@ -1913,13 +1956,13 @@ export interface UseStoreOptions { // Warning: (ae-forgotten-export) The symbol "UseStyles" needs to be exported by the entry point index.d.ts // // @public -export const useStyles$: (first: string) => UseStyles; +export const useStyles$: (qrl: string) => UseStyles; // @public export const useStylesQrl: (styles: QRL) => UseStyles; // @public -export const useStylesScoped$: (first: string) => UseStylesScoped; +export const useStylesScoped$: (qrl: string) => UseStylesScoped; // @public (undocumented) export interface UseStylesScoped { @@ -1931,7 +1974,7 @@ export interface UseStylesScoped { export const useStylesScopedQrl: (styles: QRL) => UseStylesScoped; // @public -export const useTask$: (first: TaskFn, opts?: UseTaskOptions | undefined) => void; +export const useTask$: (qrl: TaskFn, opts?: UseTaskOptions | undefined) => void; // @public (undocumented) export interface UseTaskOptions { @@ -1942,7 +1985,7 @@ export interface UseTaskOptions { export const useTaskQrl: (qrl: QRL, opts?: UseTaskOptions) => void; // @public -export const useVisibleTask$: (first: TaskFn, opts?: OnVisibleTaskOptions | undefined) => void; +export const useVisibleTask$: (qrl: TaskFn, opts?: OnVisibleTaskOptions | undefined) => void; // @public export const useVisibleTaskQrl: (qrl: QRL, opts?: OnVisibleTaskOptions) => void; @@ -2077,7 +2120,7 @@ export interface WebViewHTMLAttributes extends HTMLAttributes export function withLocale(locale: string, fn: () => T): T; // @internal (undocumented) -export const _wrapProp: , P extends keyof T>(obj: T, prop: P) => any; +export const _wrapProp: , P extends keyof T>(obj: T, prop?: P | undefined) => any; // (No @packageDocumentation comment for this package) diff --git a/packages/qwik/src/core/component/component.public.ts b/packages/qwik/src/core/component/component.public.ts index 3bb62b32743..608b7d16cb0 100644 --- a/packages/qwik/src/core/component/component.public.ts +++ b/packages/qwik/src/core/component/component.public.ts @@ -1,6 +1,5 @@ import { dollar, type PropFnInterface, type QRL } from '../qrl/qrl.public'; -import type { JSXNode, JSXOutput } from '../render/jsx/types/jsx-node'; -import { OnRenderProp, QSlot } from '../util/markers'; +import type { JSXOutput } from '../render/jsx/types/jsx-node'; import type { ComponentBaseProps, EventHandler, @@ -8,12 +7,8 @@ import type { QRLEventHandlerMulti, } from '../render/jsx/types/jsx-qwik-attributes'; import type { FunctionComponent } from '../render/jsx/types/jsx-node'; -import { Virtual } from '../render/jsx/jsx-runtime'; import { SERIALIZABLE_STATE } from '../container/serializers'; -import { qTest } from '../util/qdev'; -import { assertQrl } from '../qrl/qrl-class'; import { _CONST_PROPS, _VAR_PROPS, _jsxSorted } from '../internal'; -import { assertNumber } from '../error/assert'; import type { QwikIntrinsicElements } from '../render/jsx/types/jsx-qwik-elements'; // TS way to check for any @@ -186,25 +181,7 @@ export const componentQrl = >( componentQrl: QRL> ): Component => { // Return a QComponent Factory function. - // This is only used in v1, in v2 we use componentQrl directly - function QwikComponent(props: PublicProps, key: string | null, flags: number): JSXNode { - assertQrl(componentQrl); - assertNumber(flags, 'The Qwik Component was not invoked correctly'); - const hash = qTest ? 'sX' : componentQrl.$hash$.slice(0, 4); - const finalKey = hash + ':' + (key ? key : ''); - return _jsxSorted( - Virtual, - { - props, - [OnRenderProp]: componentQrl, - [QSlot]: props[QSlot], - }, - null, - props.children, - flags, - finalKey - ) as any; - } + const QwikComponent = () => {}; (QwikComponent as any)[SERIALIZABLE_STATE] = [componentQrl]; return QwikComponent as any; }; diff --git a/packages/qwik/src/core/components/prefetch.unit.tsx b/packages/qwik/src/core/components/prefetch.unit.tsx index 0b9647a9eb9..9d9c79cdc9c 100644 --- a/packages/qwik/src/core/components/prefetch.unit.tsx +++ b/packages/qwik/src/core/components/prefetch.unit.tsx @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { PrefetchServiceWorker, PrefetchGraph } from './prefetch'; import { renderToString2 as renderToString } from '../../server/v2-ssr-render2'; +import { cleanupAttrs } from '../../testing/element-fixture'; describe('PrefetchServiceWorker', () => { describe('render', () => { @@ -12,9 +13,7 @@ describe('PrefetchServiceWorker', () => { const output = await renderToString(, { containerTagName: 'div', }); - expect(output.html).to.contain( - ' will escape the content, effectively breaking the inlined JS. -In order to disable content escaping use ' * @@ -49,6 +56,11 @@ import type { ContainerElement, ElementVNode, QDocument } from './types'; export function processVNodeData(document: Document) { const Q_CONTAINER = 'q:container'; const Q_CONTAINER_END = '/' + Q_CONTAINER; + const Q_PROPS_SEPARATOR = ':'; + const Q_IGNORE = 'q:ignore'; + const Q_IGNORE_END = '/' + Q_IGNORE; + const Q_CONTAINER_ISLAND = 'q:container-island'; + const Q_CONTAINER_ISLAND_END = '/' + Q_CONTAINER_ISLAND; const qDocument = document as QDocument; const vNodeDataMap = qDocument.qVNodeData || (qDocument.qVNodeData = new WeakMap()); @@ -66,6 +78,7 @@ export function processVNodeData(document: Document) { ); }; const getAttribute = prototype.getAttribute as (this: Node, name: string) => string | null; + const hasAttribute = prototype.hasAttribute as (this: Node, name: string) => boolean; const getNodeType = getter(prototype, 'nodeType') as (this: Node) => number; // Process all of the `qwik/vnode` script tags by attaching them to the corresponding containers. @@ -81,12 +94,16 @@ export function processVNodeData(document: Document) { /////////////////////////////// const enum NodeType { - CONTAINER_MASK /* ******* */ = 0b0001, - ELEMENT /* ************** */ = 0b0010, // regular element - ELEMENT_CONTAINER /* **** */ = 0b0011, // container element need to descend into it - COMMENT_SKIP_START /* *** */ = 0b0101, // Comment but skip the content until COMMENT_SKIP_END - COMMENT_SKIP_END /* ***** */ = 0b1000, // Comment end - OTHER /* **************** */ = 0b0000, + CONTAINER_MASK /* ***************** */ = 0b00000001, + ELEMENT /* ************************ */ = 0b00000010, // regular element + ELEMENT_CONTAINER /* ************** */ = 0b00000011, // container element need to descend into it + COMMENT_SKIP_START /* ************* */ = 0b00000101, // Comment but skip the content until COMMENT_SKIP_END + COMMENT_SKIP_END /* *************** */ = 0b00001000, // Comment end + COMMENT_IGNORE_START /* *********** */ = 0b00010000, // Comment ignore, descend into children and skip the content until COMMENT_ISLAND_START + COMMENT_IGNORE_END /* ************* */ = 0b00100000, // Comment ignore end + COMMENT_ISLAND_START /* *********** */ = 0b01000001, // Comment island, count elements for parent container until COMMENT_ISLAND_END + COMMENT_ISLAND_END /* ************* */ = 0b10000000, // Comment island end + OTHER /* ************************** */ = 0b00000000, } /** @@ -98,11 +115,24 @@ export function processVNodeData(document: Document) { const nodeType = getNodeType.call(node); if (nodeType === 1 /* Node.ELEMENT_NODE */) { const qContainer = getAttribute.call(node, Q_CONTAINER); - return qContainer === null ? NodeType.ELEMENT : NodeType.ELEMENT_CONTAINER; + if (qContainer === null) { + const isQElement = hasAttribute.call(node, Q_PROPS_SEPARATOR); + return isQElement ? NodeType.ELEMENT : NodeType.OTHER; + } else { + return NodeType.ELEMENT_CONTAINER; + } } else if (nodeType === 8 /* Node.COMMENT_NODE */) { const nodeValue = node.nodeValue || ''; // nodeValue is monomorphic so it does not need fast path - if (nodeValue.startsWith(Q_CONTAINER)) { + if (nodeValue.startsWith(Q_CONTAINER_ISLAND)) { + return NodeType.COMMENT_ISLAND_START; + } else if (nodeValue.startsWith(Q_IGNORE)) { + return NodeType.COMMENT_IGNORE_START; + } else if (nodeValue.startsWith(Q_CONTAINER)) { return NodeType.COMMENT_SKIP_START; + } else if (nodeValue.startsWith(Q_CONTAINER_ISLAND_END)) { + return NodeType.COMMENT_ISLAND_END; + } else if (nodeValue.startsWith(Q_IGNORE_END)) { + return NodeType.COMMENT_IGNORE_END; } else if (nodeValue.startsWith(Q_CONTAINER_END)) { return NodeType.COMMENT_SKIP_END; } @@ -110,7 +140,8 @@ export function processVNodeData(document: Document) { return NodeType.OTHER; }; - const isSeparator = (ch: number) => /* `!` */ 33 <= ch && ch <= 47; /* `/` */ + const isSeparator = (ch: number) => + /* `!` */ VNodeDataSeparator.ADVANCE_1 <= ch && ch <= VNodeDataSeparator.ADVANCE_8192; /* `.` */ /** * Given the `vData` string, `start` index, and `end` index, find the end of the VNodeData * section. @@ -211,6 +242,24 @@ export function processVNodeData(document: Document) { container.qVNodeRefs!, prefix + ' ' ); + } else if (nodeType === NodeType.COMMENT_IGNORE_START) { + let islandNode = node; + do { + islandNode = walker.nextNode(); + if (!islandNode) { + throw new Error(`Island inside not found!`); + } + } while (getFastNodeType(islandNode) !== NodeType.COMMENT_ISLAND_START); + nextNode = null; + } else if (nodeType === NodeType.COMMENT_ISLAND_END) { + nextNode = node; + do { + nextNode = walker.nextNode(); + if (!nextNode) { + throw new Error(`Ignore block not closed!`); + } + } while (getFastNodeType(nextNode) !== NodeType.COMMENT_IGNORE_END); + nextNode = null; } else if (nodeType === NodeType.COMMENT_SKIP_START) { // If we are in a container, we need to skip the children. nextNode = node; @@ -245,7 +294,6 @@ export function processVNodeData(document: Document) { ch = VNodeDataSeparator.ADVANCE_1; } } - vData_end = vData_start; vData_end = findVDataSectionEnd(vData, vData_start, vData_length); } else { vNodeElementIndex = Number.MAX_SAFE_INTEGER; diff --git a/packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx b/packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx index 480dc444d3f..6c0b10618fe 100644 --- a/packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx +++ b/packages/qwik/src/core/v2/client/process-vnode-data.unit.tsx @@ -10,8 +10,8 @@ describe('processVnodeData', () => { it('should parse simple case', () => { const [container] = process(` - - + + HelloWorld ${encodeVNode({ 2: 'FF' })} @@ -30,10 +30,10 @@ describe('processVnodeData', () => { it('should ignore inner HTML', () => { const [container] = process(` - - + +
    - HelloWorld + HelloWorld ${encodeVNode({ 2: '2', 4: 'FF' })} @@ -51,18 +51,45 @@ describe('processVnodeData', () => { ); }); + + it('should ignore elements without `:`', async () => { + const [container] = process(` + + + +
    +
    ignore this
    + HelloWorld + ${encodeVNode({ 2: '3', 4: 'FF' })} + + + `); + expect(container.rootVNode).toMatchVDOM( + + + +
    +
    ignore this
    + + {'Hello'} + {'World'} + + + + ); + }); describe('nested containers', () => { it('should parse', () => { const [container1, container2] = process(` - - + + Before
    - FooBar! + FooBar! ${encodeVNode({ 0: 'D1', 1: 'DB' })}
    - After! + After! ${encodeVNode({ 2: 'G2', 4: 'FB' })} `); @@ -91,15 +118,15 @@ describe('processVnodeData', () => { }); it('should ignore comments and comment blocks', () => { const [container1] = process(` - - - + + + Before FooBar! - After! + After! ${encodeVNode({ 2: 'G1', 3: 'FB' })} `); @@ -117,6 +144,37 @@ describe('processVnodeData', () => { ); }); }); + it('should not ignore island inside comment q:container', () => { + const [container1] = process(` + + + + Before + + FooBar! + + + + AbcdAbcd! + + After! + ${encodeVNode({ 2: 'G2', 4: 'FB' })} + + `); + expect(container1.rootVNode).toMatchVDOM( + + + + {'Before'} + + + {'After'} + {'!'} + + + + ); + }); }); const qContainerPaused = { 'q:container': 'paused' }; @@ -149,7 +207,7 @@ function emitVNodeSeparators(lastSerializedIdx: number, elementIdx: number): str let skipCount = elementIdx - lastSerializedIdx; // console.log('emitVNodeSeparators', lastSerializedIdx, elementIdx, skipCount); while (skipCount != 0) { - if (skipCount >= 4096) { + if (skipCount > 4096) { result += VNodeDataSeparator.ADVANCE_8192_CH; skipCount -= 8192; } else { diff --git a/packages/qwik/src/core/v2/client/vnode-diff.ts b/packages/qwik/src/core/v2/client/vnode-diff.ts index b4b43078ecf..1cec5f6c2d1 100644 --- a/packages/qwik/src/core/v2/client/vnode-diff.ts +++ b/packages/qwik/src/core/v2/client/vnode-diff.ts @@ -11,7 +11,7 @@ import type { JSXNode, JSXOutput } from '../../render/jsx/types/jsx-node'; import type { JSXChildren } from '../../render/jsx/types/jsx-qwik-attributes'; import { SSRComment, SSRRaw, SkipRender } from '../../render/jsx/utils.public'; import { trackSignal2 } from '../../use/use-core'; -import { TaskFlags, cleanupTask, isTask, type SubscriberEffect } from '../../use/use-task'; +import { TaskFlags, cleanupTask, isTask } from '../../use/use-task'; import { EMPTY_OBJ } from '../../util/flyweight'; import { ELEMENT_KEY, @@ -27,7 +27,6 @@ import { } from '../../util/markers'; import { isPromise } from '../../util/promises'; import { type ValueOrPromise } from '../../util/types'; -import { executeComponent2 } from '../shared/component-execution'; import { convertEventNameFromJsxPropToHtmlAttr, getEventNameFromJsxProp, @@ -37,7 +36,13 @@ import { } from '../shared/event-names'; import { ChoreType } from '../shared/scheduler'; import { hasClassAttr } from '../shared/scoped-styles'; -import type { QElement2, QwikLoaderEventScope, fixMeAny } from '../shared/types'; +import type { + HostElement, + QElement2, + QwikLoaderEventScope, + fixMeAny, + qWindow, +} from '../shared/types'; import { DEBUG_TYPE, QContainerValue, VirtualType } from '../shared/types'; import type { DomContainer } from './dom-container'; import { @@ -83,11 +88,15 @@ import { vnode_setProp, vnode_setText, vnode_truncate, + vnode_walkVNode, type VNodeJournal, } from './vnode'; import { getNewElementNamespaceData } from './vnode-namespace'; import { DerivedSignal2, EffectProperty, isSignal2 } from '../signal/v2-signal'; import type { Signal2 } from '../signal/v2-signal.public'; +import { executeComponent2 } from '../shared/component-execution'; +import { isParentSlotProp, isSlotProp } from '../../util/prop'; +import { escapeHTML } from '../shared/character-escaping'; export type ComponentQueue = Array; @@ -97,7 +106,7 @@ export const vnode_diff = ( vStartNode: VNode, scopedStyleIdPrefix: string | null ) => { - const journal = (container as DomContainer).$journal$; + let journal = (container as DomContainer).$journal$; /** * Stack is used to keep track of the state of the traversal. @@ -223,6 +232,7 @@ export const vnode_diff = ( } } else if (jsxValue === SkipRender) { // do nothing, we are skipping this node + journal = []; } else { expectText(''); } @@ -382,12 +392,28 @@ export const vnode_diff = ( ///////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////// - function descendContentToProject(children: JSXChildren) { + function descendContentToProject(children: JSXChildren, host: VirtualVNode | null) { if (!Array.isArray(children)) { children = [children]; } if (children.length) { + const createProjectionJSXNode = (slotName: string) => { + return new JSXNodeImpl(Projection, EMPTY_OBJ, null, [], 0, slotName); + }; + const projections: Array = []; + if (host) { + // we need to create empty projections for all the slots to remove unused slots content + for (let i = vnode_getPropStartIndex(host); i < host.length; i = i + 2) { + const prop = host[i] as string; + if (isSlotProp(prop)) { + const slotName = prop; + projections.push(slotName); + projections.push(createProjectionJSXNode(slotName)); + } + } + } + /// STEP 1: Bucketize the children based on the projection name. for (let i = 0; i < children.length; i++) { const child = children[i]; @@ -397,14 +423,12 @@ export const vnode_diff = ( if (idx >= 0) { jsxBucket = projections[idx + 1] as any; } else { - projections.splice( - ~idx, - 0, - slotName, - (jsxBucket = new JSXNodeImpl(Projection, EMPTY_OBJ, null, [], 0, slotName)) - ); + projections.splice(~idx, 0, slotName, (jsxBucket = createProjectionJSXNode(slotName))); + } + const removeProjection = child === false; + if (!removeProjection) { + (jsxBucket.children as JSXChildren[]).push(child); } - (jsxBucket.children as JSXChildren[]).push(child); } /// STEP 2: remove the names for (let i = projections.length - 2; i >= 0; i = i - 2) { @@ -489,7 +513,12 @@ export const vnode_diff = ( if (constProps && typeof constProps == 'object' && 'name' in constProps) { const constValue = constProps.name; if (constValue instanceof DerivedSignal2) { - return trackSignal2(() => constValue.value, vHost as fixMeAny, EffectProperty.COMPONENT, container); + return trackSignal2( + () => constValue.value, + vHost as fixMeAny, + EffectProperty.COMPONENT, + container + ); } } return jsxValue.props.name || QDefaultSlot; @@ -548,9 +577,14 @@ export const vnode_diff = ( } } - /** @param tag Returns true if `qDispatchEvent` needs patching */ - function createNewElement(jsx: JSXNode, tag: string): boolean { - const element = createElementWithNamespace(tag); + /** + * Returns whether `qDispatchEvent` needs patching. This is true when one of the `jsx` argument's + * const props has the name of an event. + * + * @returns {boolean} + */ + function createNewElement(jsx: JSXNode, elementName: string): boolean { + const element = createElementWithNamespace(elementName); const { constProps } = jsx; let needsQDispatchEventPatch = false; @@ -570,16 +604,30 @@ export const vnode_diff = ( HANDLER_PREFIX + ':' + scope + ':' + eventName, value ); + if (eventName) { + registerQwikLoaderEvent(eventName); + } needsQDispatchEventPatch = true; continue; } - if (isSignal2(value)) { - if (key === 'ref') { + if (key === 'ref') { + if (isSignal2(value)) { value.value = element; continue; + } else if (typeof value === 'function') { + value(element); + continue; } - value = trackSignal2(() => (value as Signal2).value, vNewNode as fixMeAny, key, container); + } + + if (isSignal2(value)) { + value = trackSignal2( + () => (value as Signal2).value, + vNewNode as fixMeAny, + key, + container + ); } if (key === dangerouslySetInnerHTML) { @@ -588,14 +636,14 @@ export const vnode_diff = ( continue; } - if (tag === 'textarea' && key === 'value') { + if (elementName === 'textarea' && key === 'value') { if (typeof value !== 'string') { if (isDev) { throw new Error('The value of the textarea must be a string'); } continue; } - (element as HTMLTextAreaElement).value = value as string; + (element as HTMLTextAreaElement).value = escapeHTML(value as string); continue; } @@ -623,32 +671,32 @@ export const vnode_diff = ( return needsQDispatchEventPatch; } - function createElementWithNamespace(tag: string): Element { + function createElementWithNamespace(elementName: string): Element { const domParentVNode = vnode_getDomParentVNode(vParent); const { elementNamespace, elementNamespaceFlag } = getNewElementNamespaceData( domParentVNode, - tag + elementName ); - const element = container.document.createElementNS(elementNamespace, tag); - vNewNode = vnode_newElement(element, tag); + const element = container.document.createElementNS(elementNamespace, elementName); + vNewNode = vnode_newElement(element, elementName); vNewNode[VNodeProps.flags] |= elementNamespaceFlag; return element; } - function expectElement(jsx: JSXNode, tag: string) { - const isSameTagName = - vCurrent && vnode_isElementVNode(vCurrent) && tag === vnode_getElementName(vCurrent); + function expectElement(jsx: JSXNode, elementName: string) { + const isSameElementName = + vCurrent && vnode_isElementVNode(vCurrent) && elementName === vnode_getElementName(vCurrent); const jsxKey: string | null = jsx.key; let needsQDispatchEventPatch = false; - if (!isSameTagName || jsxKey !== vnode_getProp(vCurrent as ElementVNode, ELEMENT_KEY, null)) { + if (!isSameElementName || jsxKey !== getKey(vCurrent)) { // So we have a key and it does not match the current node. // We need to do a forward search to find it. // The complication is that once we start taking nodes out of order we can't use `vnode_getNextSibling` - vNewNode = retrieveChildWithKey(tag, jsxKey); + vNewNode = retrieveChildWithKey(elementName, jsxKey); if (vNewNode === null) { // No existing node with key exists, just create a new one. - needsQDispatchEventPatch = createNewElement(jsx, tag); + needsQDispatchEventPatch = createNewElement(jsx, elementName); } else { // Existing keyed node vnode_insertBefore(journal, vParent as ElementVNode, vNewNode, vCurrent); @@ -711,10 +759,18 @@ export const vnode_diff = ( vnode_setProp(vnode, key, value); return; } + if (key === 'ref') { - value.value = vnode_getNode(vnode); - return; + const element = vnode_getNode(vnode) as Element; + if (isSignal2(value)) { + value.value = element; + return; + } else if (typeof value === 'function') { + value(element); + return; + } } + vnode_setAttr(journal, vnode, key, value); if (value === null) { // if we set `null` than attribute was removed and we need to shorten the dstLength @@ -738,7 +794,9 @@ export const vnode_diff = ( } // register an event for qwik loader - ((globalThis as fixMeAny).qwikevents ||= []).push(eventName); + if (eventName) { + registerQwikLoaderEvent(eventName); + } }; while (srcKey !== null || dstKey !== null) { @@ -751,10 +809,11 @@ export const vnode_diff = ( // Source has more keys, so we need to remove them from destination if (dstKey && isHtmlAttributeAnEventName(dstKey)) { patchEventDispatch = true; + dstIdx++; } else { record(dstKey!, null); + dstIdx--; } - dstIdx++; // skip the destination value, we don't care about it. dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; } else if (dstKey == null) { // Destination has more keys, so we need to insert them from source. @@ -785,23 +844,36 @@ export const vnode_diff = ( } else { record(srcKey, srcAttrs[srcIdx]); } + srcIdx++; // advance srcValue srcKey = srcIdx < srcLength ? srcAttrs[srcIdx++] : null; + // we need to increment dstIdx too, because we added destination key and value to the VNode + // and dstAttrs is a reference to the VNode + dstIdx++; + dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; } else { // Source is missing the key, so we need to remove it from destination. if (isHtmlAttributeAnEventName(dstKey)) { patchEventDispatch = true; + dstIdx++; } else { record(dstKey!, null); + dstIdx--; } - dstIdx++; // skip the destination value, we don't care about it. dstKey = dstIdx < dstLength ? dstAttrs[dstIdx++] : null; } } return patchEventDispatch; } + function registerQwikLoaderEvent(eventName: string) { + const window = container.document.defaultView as qWindow | null; + if (window) { + (window.qwikevents ||= [] as string[]).push(eventName); + } + } + /** * Retrieve the child with the given key. * @@ -897,7 +969,7 @@ export const vnode_diff = ( function expectComponent(component: Function) { const componentMeta = (component as any)[SERIALIZABLE_STATE] as [QRLInternal>]; - let host = (vNewNode || vCurrent) as VirtualVNode; + let host = (vNewNode || vCurrent) as VirtualVNode | null; if (componentMeta) { const jsxProps = jsxValue.props; // QComponent @@ -937,31 +1009,27 @@ export const vnode_diff = ( } } - const vNodeProps = vnode_getProp(host, ELEMENT_PROPS, container.$getObjectById$); - shouldRender = shouldRender || propsDiffer(jsxProps, vNodeProps); - if (shouldRender) { - container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, jsxProps); + if (host) { + const vNodeProps = vnode_getProp(host, ELEMENT_PROPS, container.$getObjectById$); + shouldRender = shouldRender || propsDiffer(jsxProps, vNodeProps); + if (shouldRender) { + container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, jsxProps); + } } - jsxValue.children != null && descendContentToProject(jsxValue.children); + jsxValue.children != null && descendContentToProject(jsxValue.children, host); } else { // Inline Component - if (!host) { - // We did not find the component, create it. - vnode_insertBefore( - journal, - vParent as VirtualVNode, - (vNewNode = vnode_newVirtual()), - vCurrent && getInsertBefore() - ); - host = vNewNode; - } - isDev && - vnode_setProp( - (vNewNode || vCurrent) as VirtualVNode, - DEBUG_TYPE, - VirtualType.InlineComponent - ); - let component$Host: VNode = host; + vnode_insertBefore( + journal, + vParent as VirtualVNode, + (vNewNode = vnode_newVirtual()), + vCurrent && getInsertBefore() + ); + isDev && vnode_setProp(vNewNode, DEBUG_TYPE, VirtualType.InlineComponent); + vnode_setProp(vNewNode, ELEMENT_PROPS, jsxValue.propsC); + + host = vNewNode; + let component$Host: VNode | null = host; // Find the closest component host which has `OnRender` prop. while ( component$Host && @@ -969,13 +1037,13 @@ export const vnode_diff = ( ? vnode_getProp(component$Host, OnRenderProp, null) === null : true) ) { - component$Host = vnode_getParent(component$Host)!; + component$Host = vnode_getParent(component$Host); } const jsxOutput = executeComponent2( container, host, - (component$Host || container.rootVNode) as fixMeAny, - component as OnRenderFn, + (component$Host || container.rootVNode) as HostElement, + component as OnRenderFn, jsxValue.propsC ); asyncQueue.push(jsxOutput, host); @@ -1002,7 +1070,7 @@ export const vnode_diff = ( if (host) { for (let i = vnode_getPropStartIndex(host); i < host.length; i = i + 2) { const prop = host[i] as string; - if (!prop.startsWith('q:')) { + if (isSlotProp(prop)) { const value = host[i + 1]; container.setHostProp(vNewNode, prop, value); } @@ -1138,14 +1206,12 @@ export function cleanup(container: ClientContainer, vNode: VNode) { // Only elements and virtual nodes need to be traversed for children if (type & VNodeFlags.Virtual) { // Only virtual nodes have subscriptions - container.$subsManager$.$clearSub$(vCursor as fixMeAny); const seq = container.getHostProp>(vCursor as fixMeAny, ELEMENT_SEQ); if (seq) { for (let i = 0; i < seq.length; i++) { const obj = seq[i]; if (isTask(obj)) { - const task = obj as SubscriberEffect; - container.$subsManager$.$clearSub$(task); + const task = obj; if (obj.$flags$ & TaskFlags.VISIBLE_TASK) { container.$scheduler$(ChoreType.CLEANUP_VISIBLE, obj); } else { @@ -1164,8 +1230,7 @@ export function cleanup(container: ClientContainer, vNode: VNode) { const attrs = vCursor; for (let i = VirtualVNodeProps.PROPS_OFFSET; i < attrs.length; i = i + 2) { const key = attrs[i] as string; - if (!key.startsWith(':') && !key.startsWith('q:')) { - // any prop which does not start with `:` or `q:` is a content-projection prop. + if (!isParentSlotProp(key) && isSlotProp(key)) { const value = attrs[i + 1]; if (value) { attrs[i + 1] = null; // prevent infinite loop @@ -1195,6 +1260,17 @@ export function cleanup(container: ClientContainer, vNode: VNode) { vCursor = vFirstChild; continue; } + } else if (vCursor === vNode) { + /** + * If it is a projection and we are at the root, then we should only walk the children to + * materialize the projection content. This is because we could have references in the vnode + * refs map which need to be materialized before cleanup. + */ + const vFirstChild = vnode_getFirstChild(vCursor); + if (vFirstChild) { + vnode_walkVNode(vFirstChild); + return; + } } } // Out of children @@ -1208,10 +1284,7 @@ export function cleanup(container: ClientContainer, vNode: VNode) { vCursor = vNextSibling; continue; } - if (vCursor === vNode) { - // we are back where we started, we are done. - return; - } + // Out of siblings, go to parent vParent = vnode_getParent(vCursor); while (vParent) { diff --git a/packages/qwik/src/core/v2/client/vnode-diff.unit.tsx b/packages/qwik/src/core/v2/client/vnode-diff.unit.tsx index 880cf9fab8b..f68caf59e41 100644 --- a/packages/qwik/src/core/v2/client/vnode-diff.unit.tsx +++ b/packages/qwik/src/core/v2/client/vnode-diff.unit.tsx @@ -127,47 +127,56 @@ describe('vNode-diff', () => { vnode_applyJournal(journal); expect(vNode).toMatchVDOM(test); }); + it('should remove extra text node', async () => { + const { vNode, vParent, document } = vnode_fromJSX( + + {'before'} + + {'after'} + + ); + const test = ( + + + + ); + vnode_diff( + { $journal$: journal, $scheduler$: scheduler, document } as any, + test, + vParent, + null + ); + vnode_applyJournal(journal); + expect(vNode).toMatchVDOM(test); + await expect(document.querySelector('test')).toMatchDOM(test); + }); + }); + + describe('attributes', () => { it('should update attributes', () => { - // here we need tu "emulate" the var props const { vNode, vParent, document } = vnode_fromJSX( _jsxSorted( - 'test', - {}, + 'span', + { + about: 'foo', + id: 'a', + 'on:click': () => null, + }, null, - [ - _jsxSorted( - 'span', - { - id: 'a', - about: 'name', - }, - null, - [], - 0, - null - ), - ], + [], 0, null ) ); const test = _jsxSorted( - 'test', - {}, + 'span', + { + about: 'bar', + id: 'b', + onClick: () => null, + }, null, - [ - _jsxSorted( - 'span', - { - class: 'B', - about: 'ABOUT', - }, - null, - [], - 0, - null - ), - ], + [], 0, null ); @@ -175,30 +184,228 @@ describe('vNode-diff', () => { vnode_applyJournal(journal); expect(vNode).toMatchVDOM(test); }); - it('should remove extra text node', async () => { + + it('should remove attributes - case 1', () => { const { vNode, vParent, document } = vnode_fromJSX( - - {'before'} - - {'after'} - + _jsxSorted( + 'span', + { + about: 'name', + id: 'a', + test: 'value', + }, + null, + [], + 0, + null + ) ); - const test = ( - - - + const test = _jsxSorted( + 'span', + { + about: 'name', + }, + null, + [], + 0, + null ); - vnode_diff( - { $journal$: journal, $scheduler$: scheduler, document } as any, - test, - vParent, + vnode_diff({ $journal$: journal, document } as any, test, vParent, null); + vnode_applyJournal(journal); + expect(vNode).toMatchVDOM(test); + }); + + it('should remove attributes - case 2', () => { + const { vNode, vParent, document } = vnode_fromJSX( + _jsxSorted( + 'span', + { + about: 'name', + id: 'a', + test: 'value', + }, + null, + [], + 0, + null + ) + ); + const test = _jsxSorted( + 'span', + { + id: 'a', + }, + null, + [], + 0, null ); + vnode_diff({ $journal$: journal, document } as any, test, vParent, null); + vnode_applyJournal(journal); + expect(vNode).toMatchVDOM(test); + }); + + it('should remove attributes - case 3', () => { + const { vNode, vParent, document } = vnode_fromJSX( + _jsxSorted( + 'span', + { + about: 'name', + id: 'a', + test: 'value', + }, + null, + [], + 0, + null + ) + ); + const test = _jsxSorted( + 'span', + { + test: 'value', + }, + null, + [], + 0, + null + ); + vnode_diff({ $journal$: journal, document } as any, test, vParent, null); + vnode_applyJournal(journal); + expect(vNode).toMatchVDOM(test); + }); + + it('should remove attributes - case 4', () => { + const { vNode, vParent, document } = vnode_fromJSX( + _jsxSorted( + 'span', + { + about: 'name', + id: 'a', + test: 'value', + }, + null, + [], + 0, + null + ) + ); + const test = _jsxSorted('span', {}, null, [], 0, null); + vnode_diff({ $journal$: journal, document } as any, test, vParent, null); + vnode_applyJournal(journal); + expect(vNode).toMatchVDOM(test); + }); + + it('should add attributes - case 1', () => { + const { vNode, vParent, document } = vnode_fromJSX( + _jsxSorted( + 'span', + { + about: 'name', + }, + null, + [], + 0, + null + ) + ); + const test = _jsxSorted( + 'span', + { + about: 'name', + id: 'a', + test: 'value', + }, + null, + [], + 0, + null + ); + vnode_diff({ $journal$: journal, document } as any, test, vParent, null); + vnode_applyJournal(journal); + expect(vNode).toMatchVDOM(test); + }); + + it('should add attributes - case 2', () => { + const { vNode, vParent, document } = vnode_fromJSX( + _jsxSorted( + 'span', + { + id: 'a', + }, + null, + [], + 0, + null + ) + ); + const test = _jsxSorted( + 'span', + { + about: 'name', + id: 'a', + test: 'value', + }, + null, + [], + 0, + null + ); + vnode_diff({ $journal$: journal, document } as any, test, vParent, null); + vnode_applyJournal(journal); + expect(vNode).toMatchVDOM(test); + }); + + it('should add attributes - case 3', () => { + const { vNode, vParent, document } = vnode_fromJSX( + _jsxSorted( + 'span', + { + test: 'value', + }, + null, + [], + 0, + null + ) + ); + const test = _jsxSorted( + 'span', + { + about: 'name', + id: 'a', + test: 'value', + }, + null, + [], + 0, + null + ); + vnode_diff({ $journal$: journal, document } as any, test, vParent, null); + vnode_applyJournal(journal); + expect(vNode).toMatchVDOM(test); + }); + + it('should add attributes - case 4', () => { + const { vNode, vParent, document } = vnode_fromJSX(_jsxSorted('span', {}, null, [], 0, null)); + const test = _jsxSorted( + 'span', + { + about: 'name', + id: 'a', + test: 'value', + }, + null, + [], + 0, + null + ); + vnode_diff({ $journal$: journal, document } as any, test, vParent, null); vnode_applyJournal(journal); expect(vNode).toMatchVDOM(test); - await expect(document.querySelector('test')).toMatchDOM(test); }); }); + describe('keys', () => { it('should not reuse element because old has a key and new one does not', () => { const { vNode, vParent, document } = vnode_fromJSX( diff --git a/packages/qwik/src/core/v2/client/vnode-namespace.ts b/packages/qwik/src/core/v2/client/vnode-namespace.ts index 02cac30a7aa..d7db229e39f 100644 --- a/packages/qwik/src/core/v2/client/vnode-namespace.ts +++ b/packages/qwik/src/core/v2/client/vnode-namespace.ts @@ -22,21 +22,23 @@ import { type VNodeJournal, } from './vnode'; -export const isForeignObjectElement = (tag: string) => tag.toLowerCase() === 'foreignobject'; +export const isForeignObjectElement = (elementName: string) => + elementName.toLowerCase() === 'foreignobject'; -export const isSvgElement = (tag: string) => tag === 'svg' || isForeignObjectElement(tag); +export const isSvgElement = (elementName: string) => + elementName === 'svg' || isForeignObjectElement(elementName); -export const isMathElement = (tag: string) => tag === 'math'; +export const isMathElement = (elementName: string) => elementName === 'math'; export const vnode_isDefaultNamespace = (vnode: ElementVNode): boolean => { const flags = vnode[VNodeProps.flags]; return (flags & VNodeFlags.NAMESPACE_MASK) === 0; }; -export const vnode_getElementNamespaceFlags = (tag: string) => { - if (isSvgElement(tag)) { +export const vnode_getElementNamespaceFlags = (elementName: string) => { + if (isSvgElement(elementName)) { return VNodeFlags.NS_svg; - } else if (isMathElement(tag)) { + } else if (isMathElement(elementName)) { return VNodeFlags.NS_math; } else { return VNodeFlags.NS_html; @@ -203,10 +205,6 @@ function vnode_cloneElementWithNamespace( vCursor = vNextSibling; continue; } - if (vCursor === elementVNode) { - // we are back where we started, we are done. - return rootElement; - } // Out of siblings, go to parent vParent = vnode_getParent(vCursor); while (vParent) { @@ -243,7 +241,7 @@ function isMath(tagOrVNode: string | ElementVNode): boolean { export function getNewElementNamespaceData( domParentVNode: ElementVNode | null, - tag: string + elementName: string ): NewElementNamespaceData; export function getNewElementNamespaceData( domParentVNode: ElementVNode | null, diff --git a/packages/qwik/src/core/v2/client/vnode.ts b/packages/qwik/src/core/v2/client/vnode.ts index 3d95efdfa16..2a357b903e9 100644 --- a/packages/qwik/src/core/v2/client/vnode.ts +++ b/packages/qwik/src/core/v2/client/vnode.ts @@ -128,10 +128,15 @@ import { ELEMENT_KEY, ELEMENT_PROPS, ELEMENT_SEQ, + ELEMENT_SEQ_IDX, OnRenderProp, QContainerAttr, QContainerAttrEnd, + QContainerIsland, + QContainerIslandEnd, QCtxAttr, + QIgnore, + QIgnoreEnd, QScopedStyle, QSlot, QSlotParent, @@ -162,6 +167,8 @@ import { vnode_getDomChildrenWithCorrectNamespacesToInsert, vnode_getElementNamespaceFlags, } from './vnode-namespace'; +import { escapeHTML } from '../shared/character-escaping'; +import { SignalImpl } from '../../state/signal'; ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -185,7 +192,7 @@ export type VNodeJournal = Array { +export const vnode_newElement = (element: Element, elementName: string): ElementVNode => { assertEqual(fastNodeType(element), 1 /* ELEMENT_NODE */, 'Expecting element node.'); const vnode: ElementVNode = VNodeArray.createElement( VNodeFlags.Element | VNodeFlags.Inflated | (-1 << VNodeFlagsIndex.shift), // Flag @@ -195,7 +202,7 @@ export const vnode_newElement = (element: Element, tag: string): ElementVNode => null, null, element, - tag + elementName ); assertTrue(vnode_isElementVNode(vnode), 'Incorrect format of ElementVNode.'); assertFalse(vnode_isTextVNode(vnode), 'Incorrect format of ElementVNode.'); @@ -400,6 +407,53 @@ export const vnode_ensureElementInflated = (vnode: VNode) => { } }; +/** Walks the VNode tree and materialize it using `vnode_getFirstChild`. */ +export function vnode_walkVNode(vNode: VNode) { + let vCursor: VNode | null = vNode; + // Depth first traversal + if (vnode_isTextVNode(vNode)) { + // Text nodes don't have subscriptions or children; + return; + } + let vParent: VNode | null = null; + do { + const vFirstChild = vnode_getFirstChild(vCursor); + if (vFirstChild) { + vCursor = vFirstChild; + continue; + } + // Out of children + if (vCursor === vNode) { + // we are where we started, this means that vNode has no children, so we are done. + return; + } + // Out of children, go to next sibling + const vNextSibling = vnode_getNextSibling(vCursor); + if (vNextSibling) { + vCursor = vNextSibling; + continue; + } + // Out of siblings, go to parent + vParent = vnode_getParent(vCursor); + while (vParent) { + if (vParent === vNode) { + // We are back where we started, we are done. + return; + } + const vNextParentSibling = vnode_getNextSibling(vParent); + if (vNextParentSibling) { + vCursor = vNextParentSibling; + break; + } + vParent = vnode_getParent(vParent); + } + if (vParent == null) { + // We are done. + return; + } + } while (true as boolean); +} + export function vnode_getDOMChildNodes( journal: VNodeJournal, root: VNode, @@ -614,10 +668,10 @@ export const vnode_locate = (rootVNode: ElementVNode, id: string | Element): VNo refElement = id; } assertDefined(refElement, 'Missing refElement.'); - if (!Array.isArray(refElement)) { + if (!vnode_isVNode(refElement)) { assertTrue( containerElement.contains(refElement), - 'refElement must be a child of containerElement.' + `Couldn't find the element inside the container while locating the VNode.` ); // We need to find the vnode. let parent = refElement; @@ -842,7 +896,7 @@ export const vnode_applyJournal = (journal: VNodeJournal) => { if (isBooleanAttr(element, key)) { (element as any)[key] = parseBoolean(value); } else if (key === 'value' && key in element) { - (element as any).value = String(value); + (element as any).value = escapeHTML(String(value)); } else if (key === dangerouslySetInnerHTML) { (element as any).innerHTML = value!; } else { @@ -1233,6 +1287,9 @@ export const fastNextSibling = (node: Node | null): Node | null => { if (!_fastNextSibling) { _fastNextSibling = fastGetter(node, 'nextSibling')!; } + if (!_fastFirstChild) { + _fastFirstChild = fastGetter(node, 'firstChild')!; + } while (node) { node = _fastNextSibling.call(node); if (node !== null) { @@ -1240,7 +1297,12 @@ export const fastNextSibling = (node: Node | null): Node | null => { if (type === /* Node.TEXT_NODE */ 3 || type === /* Node.ELEMENT_NODE */ 1) { break; } else if (type === /* Node.COMMENT_NODE */ 8) { - if (node.nodeValue?.startsWith(QContainerAttr)) { + const nodeValue = node.nodeValue; + if (nodeValue?.startsWith(QIgnore)) { + return getNodeAfterCommentNode(node, QContainerIsland, _fastNextSibling, _fastFirstChild); + } else if (node.nodeValue?.startsWith(QContainerIslandEnd)) { + return getNodeAfterCommentNode(node, QIgnoreEnd, _fastNextSibling, _fastFirstChild); + } else if (nodeValue?.startsWith(QContainerAttr)) { while (node && (node = _fastNextSibling.call(node))) { if ( fastNodeType(node) === /* Node.COMMENT_NODE */ 8 && @@ -1256,6 +1318,41 @@ export const fastNextSibling = (node: Node | null): Node | null => { return node; }; +function getNodeAfterCommentNode( + node: Node | null, + commentValue: string, + nextSibling: NonNullable, + firstChild: NonNullable +): Node | null { + while (node) { + if (node.nodeValue?.startsWith(commentValue)) { + node = nextSibling.call(node) || null; + return node; + } + + let nextNode: Node | null = firstChild.call(node); + if (!nextNode) { + nextNode = nextSibling.call(node); + } + if (!nextNode) { + nextNode = fastParentNode(node); + if (nextNode) { + nextNode = nextSibling.call(nextNode); + } + } + node = nextNode; + } + return null; +} + +let _fastParentNode: ((this: Node) => Node | null) | null = null; +const fastParentNode = (node: Node): Node | null => { + if (!_fastParentNode) { + _fastParentNode = fastGetter(node, 'parentNode')!; + } + return _fastParentNode.call(node); +}; + let _fastFirstChild: ((this: Node) => Node | null) | null = null; const fastFirstChild = (node: Node | null): Node | null => { if (!_fastFirstChild) { @@ -1641,6 +1738,8 @@ function materializeFromVNodeData( vnode_setAttr(null, vParent, ELEMENT_KEY, consumeValue()); } else if (peek() === VNodeDataChar.SEQ) { vnode_setAttr(null, vParent, ELEMENT_SEQ, consumeValue()); + } else if (peek() === VNodeDataChar.SEQ_IDX) { + vnode_setAttr(null, vParent, ELEMENT_SEQ_IDX, consumeValue()); } else if (peek() === VNodeDataChar.CONTEXT) { vnode_setAttr(null, vParent, QCtxAttr, consumeValue()); } else if (peek() === VNodeDataChar.OPEN) { @@ -1813,10 +1912,10 @@ const VNodeArray = class VNode extends Array { firstChild: VNode | null | undefined, lastChild: VNode | null | undefined, element: Element, - tag: string | undefined + elementName: string | undefined ) { const vnode = new VNode(flags, parent, previousSibling, nextSibling) as any; - vnode.push(firstChild, lastChild, element, tag); + vnode.push(firstChild, lastChild, element, elementName); return vnode; } diff --git a/packages/qwik/src/core/v2/shared/character-escaping.ts b/packages/qwik/src/core/v2/shared/character-escaping.ts new file mode 100644 index 00000000000..378ff320559 --- /dev/null +++ b/packages/qwik/src/core/v2/shared/character-escaping.ts @@ -0,0 +1,32 @@ +export function escapeHTML(html: string): string { + let escapedHTML = ''; + const length = html.length; + let idx = 0; + let lastIdx = idx; + for (; idx < length; idx++) { + // We get the charCode NOT string. String would allocate memory. + const ch = html.charCodeAt(idx); + // Every time we concat a string we allocate memory. We want to minimize that. + if (ch === 60 /* < */) { + escapedHTML += html.substring(lastIdx, idx) + '<'; + } else if (ch === 62 /* > */) { + escapedHTML += html.substring(lastIdx, idx) + '>'; + } else if (ch === 38 /* & */) { + escapedHTML += html.substring(lastIdx, idx) + '&'; + } else if (ch === 34 /* " */) { + escapedHTML += html.substring(lastIdx, idx) + '"'; + } else if (ch === 39 /* ' */) { + escapedHTML += html.substring(lastIdx, idx) + '''; + } else { + continue; + } + lastIdx = idx + 1; + } + if (lastIdx === 0) { + // This is most common case, just return previous string no memory allocation. + return html; + } else { + // Add the tail of replacement. + return escapedHTML + html.substring(lastIdx); + } +} diff --git a/packages/qwik/src/core/v2/shared/character-escaping.unit.tsx b/packages/qwik/src/core/v2/shared/character-escaping.unit.tsx new file mode 100644 index 00000000000..95c61c5e709 --- /dev/null +++ b/packages/qwik/src/core/v2/shared/character-escaping.unit.tsx @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; +import { escapeHTML } from './character-escaping'; + +describe('character-escaping', () => { + it('escapeHTML', () => { + expect(escapeHTML('text')).toEqual('text'); + expect(escapeHTML('
    text')).toEqual( + '<div a='b' c="d">text' + ); + }); +}); diff --git a/packages/qwik/src/core/v2/shared/component-execution.ts b/packages/qwik/src/core/v2/shared/component-execution.ts index ddb45357628..c7ca956b644 100644 --- a/packages/qwik/src/core/v2/shared/component-execution.ts +++ b/packages/qwik/src/core/v2/shared/component-execution.ts @@ -1,17 +1,27 @@ +import { isDev } from '@builder.io/qwik/build'; import { isQwikComponent, type OnRenderFn } from '../../component/component.public'; import { assertDefined } from '../../error/assert'; import { isQrl, type QRLInternal } from '../../qrl/qrl-class'; import { JSXNodeImpl, isJSXNode } from '../../render/jsx/jsx-runtime'; import type { JSXNode, JSXOutput } from '../../render/jsx/types/jsx-node'; +import type { KnownEventNames } from '../../render/jsx/types/jsx-qwik-events'; import { SubscriptionType } from '../../state/common'; -import { invokeApply, newInvokeContext } from '../../use/use-core'; -import { USE_ON_LOCAL, type UseOnMap } from '../../use/use-on'; -import { SEQ_IDX_LOCAL } from '../../use/use-sequential-scope'; +import { isSignal } from '../../state/signal'; +import { invokeApply, newInvokeContext, untrack } from '../../use/use-core'; +import { type EventQRL, type UseOnMap } from '../../use/use-on'; import { EMPTY_OBJ } from '../../util/flyweight'; -import { ELEMENT_PROPS, OnRenderProp, RenderEvent } from '../../util/markers'; -import { isPromise, safeCall } from '../../util/promises'; +import { + ELEMENT_PROPS, + ELEMENT_SEQ_IDX, + OnRenderProp, + RenderEvent, + USE_ON_LOCAL, + USE_ON_LOCAL_SEQ_IDX, +} from '../../util/markers'; +import { isPromise, maybeThen, safeCall } from '../../util/promises'; import type { ValueOrPromise } from '../../util/types'; import type { Container2, HostElement, fixMeAny } from './types'; +import { logWarn } from '../../util/log'; /** * Use `executeComponent2` to execute a component. @@ -73,16 +83,19 @@ export const executeComponent2 = ( const executeComponentWithPromiseExceptionRetry = (): ValueOrPromise => safeCall( () => { - container.setHostProp(renderHost, SEQ_IDX_LOCAL, null); + container.setHostProp(renderHost, ELEMENT_SEQ_IDX, null); + container.setHostProp(renderHost, USE_ON_LOCAL_SEQ_IDX, null); container.setHostProp(renderHost, ELEMENT_PROPS, props); return componentFn(props); }, (jsx) => { const useOnEvents = container.getHostProp(renderHost, USE_ON_LOCAL); - useOnEvents && addUseOnEvents(jsx, useOnEvents); + if (useOnEvents) { + return maybeThen(addUseOnEvents(jsx, useOnEvents), () => jsx); + } return jsx; }, - (err: any) => { + (err) => { if (isPromise(err)) { return err.then(executeComponentWithPromiseExceptionRetry) as Promise; } else { @@ -105,45 +118,72 @@ export const executeComponent2 = ( * So when executing a component we only care about its last JSX Output. */ -function addUseOnEvents(jsx: JSXOutput, useOnEvents: UseOnMap) { - let jsxElement = findFirstStringJSX(jsx); - let isInvisibleComponent = false; - if (!jsxElement) { - /** - * We did not find any jsx node with a string tag. This means that we should append: - * - * ```html - * - * ``` - * - * This is needed because use on events should have a node to attach them to. - */ - isInvisibleComponent = true; - jsxElement = addScriptNodeForInvisibleComponents(jsx); +function addUseOnEvents( + jsx: JSXOutput, + useOnEvents: UseOnMap +): ValueOrPromise | null> { + const jsxElement = findFirstStringJSX(jsx); + return maybeThen(jsxElement, (jsxElement) => { + let isInvisibleComponent = false; if (!jsxElement) { - return; + /** + * We did not find any jsx node with a string tag. This means that we should append: + * + * ```html + * + * ``` + * + * This is needed because use on events should have a node to attach them to. + */ + isInvisibleComponent = true; } - } - for (const key in useOnEvents) { - if (Object.prototype.hasOwnProperty.call(useOnEvents, key)) { - let props = jsxElement.props; - if (props === EMPTY_OBJ) { - props = jsxElement.props = {}; - } - let propValue = props[key] as UseOnMap['any'] | UseOnMap['any'][0] | undefined; - if (propValue === undefined) { - propValue = []; - } else if (!Array.isArray(propValue)) { - propValue = [propValue]; + for (const key in useOnEvents) { + if (Object.prototype.hasOwnProperty.call(useOnEvents, key)) { + if (isInvisibleComponent) { + if (key === 'onQvisible$') { + jsxElement = addScriptNodeForInvisibleComponents(jsx); + if (jsxElement) { + addUseOnEvent(jsxElement, 'document:onQinit$', useOnEvents[key]); + } + } else if (key.startsWith('document:') || key.startsWith('window:')) { + jsxElement = addScriptNodeForInvisibleComponents(jsx); + if (jsxElement) { + addUseOnEvent(jsxElement, key, useOnEvents[key]); + } + } else if (isDev) { + logWarn( + 'You are trying to add an event "' + + key + + '" using `useOn` hook, ' + + 'but a node to which you can add an event is not found. ' + + 'Please make sure that the component has a valid element node. ' + ); + } + } else if (jsxElement) { + addUseOnEvent(jsxElement, key, useOnEvents[key]); + } } - propValue.push(...useOnEvents[key]); - const eventKey = isInvisibleComponent ? 'document:onQinit$' : key; - props[eventKey] = propValue; } + return jsxElement; + }); +} + +function addUseOnEvent(jsxElement: JSXNode, key: string, value: EventQRL[]) { + let props = jsxElement.props; + if (props === EMPTY_OBJ) { + props = jsxElement.props = {}; + } + let propValue = props[key] as UseOnMap['any'] | UseOnMap['any'][0] | undefined; + if (propValue === undefined) { + propValue = []; + } else if (!Array.isArray(propValue)) { + propValue = [propValue]; } + propValue.push(...value); + props[key] = propValue; } -function findFirstStringJSX(jsx: JSXOutput): JSXNode | null { +function findFirstStringJSX(jsx: JSXOutput): ValueOrPromise | null> { const queue: any[] = [jsx]; while (queue.length) { const jsx = queue.shift(); @@ -154,6 +194,10 @@ function findFirstStringJSX(jsx: JSXOutput): JSXNode | null { queue.push(jsx.children); } else if (Array.isArray(jsx)) { queue.push(...jsx); + } else if (isPromise(jsx)) { + return maybeThen | null>(jsx, (jsx) => findFirstStringJSX(jsx)); + } else if (isSignal(jsx)) { + return findFirstStringJSX(untrack(() => jsx.value as JSXOutput)); } } return null; diff --git a/packages/qwik/src/core/v2/shared/scheduler.ts b/packages/qwik/src/core/v2/shared/scheduler.ts index af4ff87d204..183e6848b30 100644 --- a/packages/qwik/src/core/v2/shared/scheduler.ts +++ b/packages/qwik/src/core/v2/shared/scheduler.ts @@ -88,22 +88,26 @@ import { Task, TaskFlags, cleanupTask, - runComputed2, - runSubscriber2, + runTask2, + runResource, + type ResourceDescriptor, type TaskFn, } from '../../use/use-task'; +import { logWarn } from '../../util/log'; import { isPromise, maybeThen, maybeThenPassError, safeCall } from '../../util/promises'; import type { ValueOrPromise } from '../../util/types'; +import { isDomContainer } from '../client/dom-container'; import type { VirtualVNode } from '../client/types'; import { vnode_documentPosition, vnode_isVNode, vnode_setAttr } from '../client/vnode'; import { vnode_diff } from '../client/vnode-diff'; import { executeComponent2 } from './component-execution'; import type { Container2, HostElement, fixMeAny } from './types'; -import { EffectSubscriptionsProp, isSignal2, type EffectSubscriptions } from '../signal/v2-signal'; +import { isSignal2 } from '../signal/v2-signal'; import { serializeAttribute } from '../../render/execute-component'; +import { type DomContainer } from '../client/dom-container'; // Turn this on to get debug output of what the scheduler is doing. -const DEBUG: boolean = true; +const DEBUG: boolean = false; export const enum ChoreType { /// MASKS defining three levels of sorting @@ -112,9 +116,7 @@ export const enum ChoreType { MICRO /* ***************** */ = 0b000_1111, /** Ensure tha the QRL promise is resolved before processing next chores in the queue */ - QRL_RESOLVE /* *********** */ = 0b000_0000, - // TODO(mhevery): COMPUTED should be deleted because it is handled synchronously. - COMPUTED /* ************** */ = 0b000_0001, + QRL_RESOLVE /* *********** */ = 0b000_0001, RESOURCE /* ************** */ = 0b000_0010, TASK /* ****************** */ = 0b000_0011, NODE_DIFF /* ************* */ = 0b000_0100, @@ -159,8 +161,7 @@ export const createScheduler = ( function schedule( type: ChoreType.QRL_RESOLVE, ignore0: null, - ignore1: null, - promise: Promise + ignore1: QRLInternal ): ValueOrPromise; function schedule(type: ChoreType.JOURNAL_FLUSH): ValueOrPromise; function schedule(type: ChoreType.WAIT_FOR_ALL): ValueOrPromise; @@ -174,7 +175,10 @@ export const createScheduler = ( * @param props- Props to pass to the component. * @param waitForChore? = false */ - function schedule(type: ChoreType.TASK | ChoreType.VISIBLE, task: Task): ValueOrPromise; + function schedule( + type: ChoreType.TASK | ChoreType.VISIBLE | ChoreType.RESOURCE, + task: Task + ): ValueOrPromise; function schedule( type: ChoreType.COMPONENT, host: HostElement, @@ -187,7 +191,6 @@ export const createScheduler = ( qrl: QRL<(...args: any[]) => any>, props: any ): ValueOrPromise; - function schedule(type: ChoreType.COMPUTED, task: Task): ValueOrPromise; function schedule( type: ChoreType.NODE_DIFF, host: HostElement, @@ -215,7 +218,7 @@ export const createScheduler = ( const isTask = type === ChoreType.TASK || type === ChoreType.VISIBLE || - type === ChoreType.COMPUTED || + type === ChoreType.RESOURCE || type === ChoreType.CLEANUP_VISIBLE; if (isTask) { (hostOrTask as Task).$flags$ |= TaskFlags.DIRTY; @@ -236,7 +239,7 @@ export const createScheduler = ( }; chore.$promise$ = new Promise((resolve) => (chore.$resolve$ = resolve)); DEBUG && debugTrace('schedule', chore, currentChore, choreQueue); - chore = sortedInsert(choreQueue, chore, choreComparator, choreUpdate); + chore = sortedInsert(choreQueue, chore); if (!journalFlushScheduled && runLater) { // If we are not currently draining, we need to schedule a drain. journalFlushScheduled = true; @@ -262,7 +265,10 @@ export const createScheduler = ( } while (choreQueue.length) { const nextChore = choreQueue.shift()!; - const comp = choreComparator(nextChore, runUptoChore); + const comp = choreComparator(nextChore, runUptoChore, false); + if (comp === null) { + continue; + } if (comp > 0) { // we have processed all of the chores up to the given chore. break; @@ -305,21 +311,19 @@ export const createScheduler = ( (err: any) => container.handleError(err, host) ); break; - case ChoreType.COMPUTED: - returnValue = runComputed2(chore.$payload$ as Task, container, host); + case ChoreType.RESOURCE: + // Don't await the return value of the resource, because async resources should not be awaited. + // The reason for this is that we should be able to update for example a node with loading + // text. If we await the resource, the loading text will not be displayed until the resource + // is loaded. + const result = runResource(chore.$payload$ as ResourceDescriptor, container, host); + returnValue = isDomContainer(container) ? null : result; break; case ChoreType.TASK: - const payload = chore.$payload$; - if (Array.isArray(payload)) { - // This is a hack to see if the scheduling will work. - const effectSubscriber = payload as fixMeAny as EffectSubscriptions; - const effect = effectSubscriber[EffectSubscriptionsProp.EFFECT]; - returnValue = runSubscriber2(effect as Task, container, host); - break; - } - // eslint-disable-next-line no-fallthrough + returnValue = runTask2(chore.$payload$ as Task, container, host); + break; case ChoreType.VISIBLE: - returnValue = runSubscriber2(chore.$payload$ as Task, container, host); + returnValue = runTask2(chore.$payload$ as Task, container, host); break; case ChoreType.CLEANUP_VISIBLE: const task = chore.$payload$ as Task; @@ -331,7 +335,7 @@ export const createScheduler = ( if (isSignal2(jsx)) { jsx = jsx.value as any; } - returnValue = vnode_diff(container as fixMeAny, jsx, parentVirtualNode, null); + returnValue = vnode_diff(container as DomContainer, jsx, parentVirtualNode, null); break; case ChoreType.NODE_PROP: const virtualNode = chore.$host$ as VirtualVNode; @@ -339,12 +343,16 @@ export const createScheduler = ( if (isSignal2(value)) { value = value.value as any; } - // TODO(mhevery): Fix this hack - const journal = (container as fixMeAny).$journal$ as fixMeAny; + const journal = (container as DomContainer).$journal$; const property = chore.$idx$ as string; value = serializeAttribute(property, value); vnode_setAttr(journal, virtualNode, property, value); break; + case ChoreType.QRL_RESOLVE: { + const target = chore.$target$ as QRLInternal; + returnValue = target.resolve(); + break; + } } return maybeThenPassError(returnValue, (value) => { DEBUG && debugTrace('execute.DONE', null, currentChore, choreQueue); @@ -355,25 +363,6 @@ export const createScheduler = ( } }; -export const hostElementPredicate = (aHost: HostElement, bHost: HostElement): number => { - if (aHost === bHost) { - return 0; - } else { - if (vnode_isVNode(aHost) && vnode_isVNode(bHost)) { - // we are running on the client. - return vnode_documentPosition(aHost, bHost); - } else { - // we are running on the server. - // On server we can't schedule task for a different host! - // Server is SSR, and therefore scheduling for anything but the current host - // implies that things need to be re-run nad that is not supported because of streaming. - throw new Error( - 'SERVER: during HTML streaming, it is not possible to cause a re-run of tasks on a different host' - ); - } - } -}; - const toNumber = (value: number | string): number => { return typeof value === 'number' ? value : -1; }; @@ -389,7 +378,9 @@ const choreUpdate = (existing: Chore, newChore: Chore): void => { } }; -export const choreComparator = (a: Chore, b: Chore): number => { +function choreComparator(a: Chore, b: Chore, shouldThrowOnHostMismatch: true): number; +function choreComparator(a: Chore, b: Chore, shouldThrowOnHostMismatch: false): number | null; +function choreComparator(a: Chore, b: Chore, shouldThrowOnHostMismatch: boolean): number | null { const macroTypeDiff = (a.$type$ & ChoreType.MACRO) - (b.$type$ & ChoreType.MACRO); if (macroTypeDiff !== 0) { return macroTypeDiff; @@ -411,9 +402,13 @@ export const choreComparator = (a: Chore, b: Chore): number => { // On server we can't schedule task for a different host! // Server is SSR, and therefore scheduling for anything but the current host // implies that things need to be re-run nad that is not supported because of streaming. - throw new Error( - 'SERVER: during HTML streaming, it is not possible to cause a re-run of tasks on a different host' - ); + const errorMessage = + 'SERVER: during HTML streaming, it is not possible to cause a re-run of tasks on a different host'; + if (shouldThrowOnHostMismatch) { + throw new Error(errorMessage); + } + logWarn(errorMessage); + return null; } } @@ -429,7 +424,7 @@ export const choreComparator = (a: Chore, b: Chore): number => { } return 0; -}; +} export const intraHostPredicate = (a: Chore, b: Chore): number => { const idxDiff = toNumber(a.$idx$) - toNumber(b.$idx$); @@ -451,11 +446,7 @@ export const intraHostPredicate = (a: Chore, b: Chore): number => { return 0; }; -function sortedFindIndex( - sortedArray: T[], - value: T, - comparator: (a: T, b: T) => number -): number { +function sortedFindIndex(sortedArray: Chore[], value: Chore): number { /// We need to ensure that the `queue` is sorted by priority. /// 1. Find a place where to insert into. let bottom = 0; @@ -463,7 +454,7 @@ function sortedFindIndex( while (bottom < top) { const middle = bottom + ((top - bottom) >> 1); const midChore = sortedArray[middle]; - const comp = comparator(value, midChore); + const comp = choreComparator(value, midChore, true); if (comp < 0) { top = middle; } else if (comp > 0) { @@ -476,22 +467,17 @@ function sortedFindIndex( return ~bottom; } -function sortedInsert( - sortedArray: T[], - value: T, - comparator: (a: T, b: T) => number, - updater?: (a: T, b: T) => void -): T { +function sortedInsert(sortedArray: Chore[], value: Chore): Chore { /// We need to ensure that the `queue` is sorted by priority. /// 1. Find a place where to insert into. - const idx = sortedFindIndex(sortedArray, value, comparator); + const idx = sortedFindIndex(sortedArray, value); if (idx < 0) { /// 2. Insert the chore into the queue. sortedArray.splice(~idx, 0, value); return value; } const existing = sortedArray[idx]; - updater && updater(existing, value); + choreUpdate(existing, value); return existing; } @@ -499,7 +485,7 @@ function debugChoreToString(chore: Chore): string { const type = ( { - [ChoreType.COMPUTED]: 'COMPUTED', + [ChoreType.QRL_RESOLVE]: 'QRL_RESOLVE', [ChoreType.RESOURCE]: 'RESOURCE', [ChoreType.TASK]: 'TASK', [ChoreType.NODE_DIFF]: 'NODE_DIFF', diff --git a/packages/qwik/src/core/v2/shared/shared-serialization.ts b/packages/qwik/src/core/v2/shared/shared-serialization.ts index ca6401faec9..630fbe733f7 100644 --- a/packages/qwik/src/core/v2/shared/shared-serialization.ts +++ b/packages/qwik/src/core/v2/shared/shared-serialization.ts @@ -1,10 +1,10 @@ -import type { FunctionComponent } from '@builder.io/qwik'; import { isDev } from '../../../build/index.dev'; import type { StreamWriter } from '../../../server/types'; import { componentQrl, isQwikComponent } from '../../component/component.public'; import type { ObjToProxyMap } from '../../container/container'; import { SERIALIZABLE_STATE } from '../../container/serializers'; import { assertDefined, assertTrue } from '../../error/assert'; +import { getPlatform } from '../../platform/platform'; import { createQRL, isQrl, @@ -21,20 +21,18 @@ import { isPropsProxy, } from '../../render/jsx/jsx-runtime'; import { Slot } from '../../render/jsx/slot.public'; -import { - fastSkipSerialize, - getProxyFlags, - getSubscriptionManager -} from '../../state/common'; +import { type FunctionComponent } from '../../render/jsx/types/jsx-node'; +import { fastSkipSerialize } from '../../state/common'; import { _CONST_PROPS, _VAR_PROPS } from '../../state/constants'; import { Task, isTask, type ResourceReturnInternal } from '../../use/use-task'; +import { isElement, isNode } from '../../util/element'; import { EMPTY_OBJ } from '../../util/flyweight'; import { throwErrorAndStop } from '../../util/log'; import { ELEMENT_ID } from '../../util/markers'; import { isPromise } from '../../util/promises'; -import type { ValueOrPromise } from '../../util/types'; -import type { DomContainer } from '../client/dom-container'; -import { vnode_isVNode, vnode_locate } from '../client/vnode'; +import { isSerializableObject, type ValueOrPromise } from '../../util/types'; +import { type DomContainer } from '../client/dom-container'; +import { vnode_getNode, vnode_isVNode, vnode_locate } from '../client/vnode'; import { ComputedSignal2, DerivedSignal2, @@ -42,15 +40,22 @@ import { Signal2, type EffectSubscriptions, } from '../signal/v2-signal'; -import { Store2, createStore2, getStoreHandler2, unwrapStore2, type StoreHandler } from '../signal/v2-store'; +import { + Store2, + createStore2, + getStoreHandler2, + getStoreTarget2, + unwrapStore2, + type StoreHandler, +} from '../signal/v2-store'; import type { SymbolToChunkResolver } from '../ssr/ssr-types'; -import type { fixMeAny } from './types'; +import type { DeserializeContainer, fixMeAny } from './types'; const deserializedProxyMap = new WeakMap(); type DeserializerProxy = T & { [SERIALIZER_PROXY_UNWRAP]: object }; -const unwrapDeserializerProxy = (value: unknown) => { +export const unwrapDeserializerProxy = (value: unknown) => { const unwrapped = typeof value === 'object' && value !== null && @@ -92,6 +97,28 @@ class DeserializationHandler implements ProxyHandler { if (property === SERIALIZER_PROXY_UNWRAP) { return target; } + if (getStoreTarget2(target) !== undefined) { + /** + * If we modify string value by for example `+=` operator, we need to get the old value first. + * If the target is a store proxy, we need to unwrap it and get the real object. This is + * because if we try to get the value, we will get deserialized value which is not what we + * want in case of string. + * + * For strings we always assume that they are not deserialized (cached), so we need to get the + * real value. The reason is that if we have a string which starts with a serialization + * constant character, we need to have the SerializationConstant.String_CHAR prefix character. + * Otherwise the system will try to deserialize the value again. + */ + const unwrapped = unwrapDeserializerProxy(unwrapStore2(target)) as object; + const unwrappedPropValue = Reflect.get(unwrapped, property, receiver); + if ( + typeof unwrappedPropValue === 'string' && + unwrappedPropValue.length >= 1 && + unwrappedPropValue.charCodeAt(0) === SerializationConstant.String_VALUE + ) { + return allocate(unwrappedPropValue); + } + } let propValue = Reflect.get(target, property, receiver); let typeCode: number; if ( @@ -146,6 +173,24 @@ class DeserializationHandler implements ProxyHandler { return propValue; } + set(target: object, property: string | symbol, newValue: any, receiver: any): boolean { + /** + * If we are setting a value which is a string and starts with a special character, we need to + * prefix it with a SerializationConstant character to indicate that it is a string. + * + * Without this later (when getting the value) we would try to deserialize the value incorrectly + * due to the special character at the start. + */ + if ( + typeof newValue === 'string' && + newValue.length >= 1 && + newValue.charCodeAt(0) < SerializationConstant.LAST_VALUE + ) { + return Reflect.set(target, property, SerializationConstant.String_CHAR + newValue, receiver); + } + return Reflect.set(target, property, newValue, receiver); + } + has(target: object, property: PropertyKey) { if (property === SERIALIZER_PROXY_UNWRAP) { return true; @@ -240,7 +285,7 @@ const restString = () => { return rest.substring(start, restIdx - 1); }; -const inflate = (container: DomContainer, target: any, needsInflationData: string) => { +const inflate = (container: DeserializeContainer, target: any, needsInflationData: string) => { restStack.push(rest, restIdx); rest = needsInflationData; restIdx = 1; @@ -266,7 +311,7 @@ const inflate = (container: DomContainer, target: any, needsInflationData: strin break; case SerializationConstant.Store_VALUE: const storeHandler = getStoreHandler2(target)!; - storeHandler.$container$ = container; + storeHandler.$container$ = container as DomContainer; storeHandler.$target$ = container.$getObjectById$(restInt()); storeHandler.$flags$ = restInt(); const effectProps = rest.substring(restIdx).split('|'); @@ -277,7 +322,7 @@ const inflate = (container: DomContainer, target: any, needsInflationData: strin const idx = effect.indexOf(';'); const prop = effect.substring(0, idx); const effectStr = effect.substring(idx + 1); - deserializeSignal2Effect(0, effectStr.split(';'), container, effects[prop] = []) + deserializeSignal2Effect(0, effectStr.split(';'), container, (effects[prop] = [])); } } break; @@ -460,12 +505,14 @@ export function parseQRL(qrl: string): QRLInternal { return createQRL(chunk, symbol, qrlRef, null, captureIds, null, null); } -export function inflateQRL(container: DomContainer, qrl: QRLInternal) { +export function inflateQRL(container: DeserializeContainer, qrl: QRLInternal) { const captureIds = qrl.$capture$; qrl.$captureRef$ = captureIds ? captureIds.map((id) => container.$getObjectById$(parseInt(id))) : null; - qrl.$setContainer$(container.element); + if (container.element) { + qrl.$setContainer$(container.element); + } return qrl; } @@ -826,14 +873,18 @@ function serialize(serializationContext: SerializationContext): void { const varId = $addRoot$(varProps); const constProps = value[_CONST_PROPS]; const constId = $addRoot$(constProps); - writeString(SerializationConstant.PropsProxy_CHAR + varId + '|' + constId); + writeString(SerializationConstant.PropsProxy_CHAR + varId + ' ' + constId); } else if ((storeHandler = getStoreHandler2(value))) { - let store = SerializationConstant.Store_CHAR + $addRoot$(storeHandler.$target$) + ' ' + storeHandler.$flags$; + let store = + SerializationConstant.Store_CHAR + + $addRoot$(storeHandler.$target$) + + ' ' + + storeHandler.$flags$; const effects = storeHandler.$effects$; if (effects) { let sep = ' '; for (const propName in effects) { - store += sep + propName + serializeEffectSubs($addRoot$, effects[propName]) + store += sep + propName + serializeEffectSubs($addRoot$, effects[propName]); sep = '|'; } } @@ -842,14 +893,7 @@ function serialize(serializationContext: SerializationContext): void { if (isResource(value)) { serializationContext.$resources$.add(value); } - serializeObjectLiteral( - value, - $writer$, - writeValue, - writeString, - serializationContext.$proxyMap$, - $addRoot$ - ); + serializeObjectLiteral(value, $writer$, writeValue, writeString); } else if (value instanceof Signal2) { if (value instanceof DerivedSignal2) { writeString( @@ -954,35 +998,15 @@ function serialize(serializationContext: SerializationContext): void { value: any, $writer$: StreamWriter, writeValue: (value: any, idx: number) => void, - writeString: (text: string) => void, - objectMap: ObjToProxyMap, - $addRoot$: (obj: unknown) => number + writeString: (text: string) => void ) => { if (Array.isArray(value)) { - const proxy = objectMap.get(value); - if (proxy !== undefined) { - $writer$.write('{'); - serializeProxy(value, proxy, $writer$, writeString, $addRoot$); - $writer$.write(','); - // for an array we have to add property key (undefined) - writeString(SerializationConstant.UNDEFINED_CHAR); - $writer$.write(':'); - } - // Serialize as array. serializeArray(value, $writer$, writeValue); - - if (proxy !== undefined) { - $writer$.write('}'); - } } else { // Serialize as object. $writer$.write('{'); - const proxy = objectMap.get(value); - if (proxy !== undefined) { - serializeProxy(value, proxy, $writer$, writeString, $addRoot$); - } - serializeObjectProperties(value, $writer$, writeValue, writeString, proxy !== undefined); + serializeObjectProperties(value, $writer$, writeValue, writeString); $writer$.write('}'); } }; @@ -1009,22 +1033,6 @@ function serializeEffectSubs( return data; } -const subscriptionManagerToString: any = null!; - -function serializeProxy( - value: any, - proxy: any, - $writer$: StreamWriter, - writeString: (text: string) => void, - $addRoot$: (obj: unknown) => number -) { - const flags = getProxyFlags(value) || 0; - writeString(SerializationConstant.Store_CHAR); - $writer$.write(':'); - const manager = getSubscriptionManager(proxy)!; - writeString(String(flags) + subscriptionManagerToString(manager, $addRoot$)); -} - function serializeArray( value: any, $writer$: StreamWriter, @@ -1044,10 +1052,9 @@ function serializeObjectProperties( value: any, $writer$: StreamWriter, writeValue: (value: any, idx: number) => void, - writeString: (text: string) => void, - startWithDelimiter: boolean + writeString: (text: string) => void ) { - let delimiter = startWithDelimiter; + let delimiter = false; for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key) && !fastSkipSerialize(value[key])) { delimiter && $writer$.write(','); @@ -1079,12 +1086,12 @@ function serializeDerivedFn( function deserializeSignal2( signal: Signal2, - container: DomContainer, + container: DeserializeContainer, data: string, readFn: boolean, readQrl: boolean ) { - signal.$container$ = container; + signal.$container$ = container as DomContainer; const parts = data.substring(1).split(';'); let idx = 0; if (readFn) { @@ -1100,16 +1107,25 @@ function deserializeSignal2( } if (readQrl) { const computedSignal = signal as ComputedSignal2; - computedSignal.$computeQrl$ = parseQRL(parts[idx++]) as fixMeAny; + computedSignal.$computeQrl$ = inflateQRL(container, parseQRL(parts[idx++])) as fixMeAny; + } + let signalValue = container.$getObjectById$(parts[idx++]); + if (vnode_isVNode(signalValue)) { + signalValue = vnode_getNode(signalValue); } - signal.$untrackedValue$ = container.$getObjectById$(parts[idx++]); + signal.$untrackedValue$ = signalValue; if (idx < parts.length) { const effects = signal.$effects$ || (signal.$effects$ = []); idx = deserializeSignal2Effect(idx, parts, container, effects); } } -function deserializeSignal2Effect(idx: number, parts: string[], container: DomContainer, effects: EffectSubscriptions[]) { +function deserializeSignal2Effect( + idx: number, + parts: string[], + container: DeserializeContainer, + effects: EffectSubscriptions[] +) { while (idx < parts.length) { // idx == 1 is the attribute name const effect = parts[idx++] @@ -1136,6 +1152,19 @@ export function qrlToString( ) { let symbol = value.$symbol$; let chunk = value.$chunk$; + + const refSymbol = value.$refSymbol$ ?? symbol; + const platform = getPlatform(); + if (platform) { + const result = platform.chunkForSymbol(refSymbol, chunk); + if (result) { + chunk = result[1]; + if (!value.$refSymbol$) { + symbol = result[0]; + } + } + } + const isSync = isSyncQrl(value); if (!isSync) { // If we have a symbol we need to resolve the chunk. @@ -1178,6 +1207,140 @@ export function qrlToString( return qrlStringInline; } +/** + * Serialize data to string using SerializationContext. + * + * @param data - Data to serialize + * @internal + */ +export async function _serialize(data: unknown[]): Promise { + const serializationContext = createSerializationContext( + null, + new WeakMap(), + () => '', + () => {} + ); + + for (const root of data) { + serializationContext.$addRoot$(root); + } + await serializationContext.$breakCircularDepsAndAwaitPromises$(); + serializationContext.$serialize$(); + return serializationContext.$writer$.toString(); +} + +/** + * Deserialize data from string to an array of objects. + * + * @param rawStateData - Data to deserialize + * @param element - Container element + * @internal + */ +export function _deserialize(rawStateData: string | null, element?: unknown): unknown[] { + if (rawStateData == null) { + return []; + } + const stateData = JSON.parse(rawStateData); + if (!Array.isArray(stateData)) { + return []; + } + + let container: DeserializeContainer | undefined = undefined; + if (isNode(element) && isElement(element)) { + container = createDeserializeContainer(stateData, element as HTMLElement); + } else { + container = createDeserializeContainer(stateData); + } + for (let i = 0; i < stateData.length; i++) { + const data = stateData[i]; + stateData[i] = deserializeData(stateData, data, container); + } + return stateData; +} + +function deserializeData( + stateData: unknown[], + serializedData: unknown, + container: DeserializeContainer +) { + let typeCode: number; + if ( + typeof serializedData === 'string' && + serializedData.length >= 1 && + (typeCode = serializedData.charCodeAt(0)) < SerializationConstant.LAST_VALUE + ) { + let propValue = serializedData; + propValue = allocate(propValue); + + if (typeCode >= SerializationConstant.Error_VALUE) { + inflate(container, propValue, serializedData); + } + return propValue; + } else if (serializedData && typeof serializedData === 'object') { + if (Array.isArray(serializedData)) { + return deserializeArray(stateData, serializedData, container); + } else { + return deserializeObject(stateData, serializedData, container); + } + } + return serializedData; +} + +function deserializeObject( + stateData: unknown[], + serializedData: object, + container: DeserializeContainer +) { + if (!isSerializableObject(serializedData)) { + return serializedData; + } + for (const key in serializedData) { + if (Object.prototype.hasOwnProperty.call(serializedData, key)) { + const value = serializedData[key]; + serializedData[key] = deserializeData(stateData, value, container); + } + } + return serializedData; +} + +function deserializeArray( + stateData: unknown[], + serializedData: Array, + container: DeserializeContainer +) { + for (let i = 0; i < serializedData.length; i++) { + const value = serializedData[i]; + serializedData[i] = deserializeData(stateData, value, container); + } + return serializedData; +} + +function getObjectById(id: number | string, stateData: unknown[]): unknown { + if (typeof id === 'string') { + id = parseFloat(id); + } + assertTrue(id < stateData.length, 'Invalid reference'); + return stateData[id]; +} + +function createDeserializeContainer( + stateData: unknown[], + element?: HTMLElement +): DeserializeContainer { + const container: DeserializeContainer = { + $getObjectById$: (id: number | string) => getObjectById(id, stateData), + getSyncFn: (_: number) => { + const fn = () => {}; + return fn; + }, + element: null, + }; + if (element) { + container.element = element; + } + return container; +} + /** * Tracking all objects in the map would be expensive. For this reason we only track some of the * objects. @@ -1260,6 +1423,12 @@ export const canSerialize2 = (value: any): boolean => { return true; } else if (isTask(value)) { return true; + } else if (value instanceof Error) { + return true; + } else if (isPromise(value)) { + return true; + } else if (isJSXNode(value)) { + return true; } } else if (typeof value === 'function') { if (isQrl(value) || isQwikComponent(value)) { @@ -1352,7 +1521,10 @@ function serializeJSXType($addRoot$: (obj: unknown) => number, type: string | Fu } } -function deserializeJSXType(container: DomContainer, type: string): string | FunctionComponent { +function deserializeJSXType( + container: DeserializeContainer, + type: string +): string | FunctionComponent { if (type === ':slot') { return Slot; } else if (type === ':fragment') { diff --git a/packages/qwik/src/core/v2/shared/shared-serialization.unit.ts b/packages/qwik/src/core/v2/shared/shared-serialization.unit.ts index c82dcd87d14..40509fc5b4c 100644 --- a/packages/qwik/src/core/v2/shared/shared-serialization.unit.ts +++ b/packages/qwik/src/core/v2/shared/shared-serialization.unit.ts @@ -1,7 +1,13 @@ import { describe, it, expect } from 'vitest'; -import { SerializationConstant, createSerializationContext } from './shared-serialization'; +import { + SerializationConstant, + _deserialize, + _serialize, + createSerializationContext, +} from './shared-serialization'; import { Task } from '../../use/use-task'; import { inlinedQrl } from '../../qrl/qrl'; +import { isQrl } from '../../qrl/qrl-class'; const DEBUG = false; @@ -60,6 +66,63 @@ describe('shared-serialization', () => { ]); }); }); + + describe('server side serialization', () => { + it('should serialize data', async () => { + const serializedData = await _serialize([ + inlinedQrl(0, 'Root_component_arKLnchfR8k'), + undefined, + new URL('http://example.com'), + ]); + + const stateData = JSON.stringify([ + SerializationConstant.QRL_CHAR + 'qwik-runtime-mock-chunk#Root_component_arKLnchfR8k', + SerializationConstant.UNDEFINED_CHAR, + SerializationConstant.URL_CHAR + 'http://example.com/', + ]); + expect(serializedData).toEqual(stateData); + }); + + it('should serialize nested data', async () => { + const serializedData = await _serialize([ + { foo: new URL('http://example.com'), bar: [undefined] }, + ]); + + const stateData = JSON.stringify([ + { + foo: SerializationConstant.URL_CHAR + 'http://example.com/', + bar: [SerializationConstant.UNDEFINED_CHAR], + }, + ]); + expect(serializedData).toEqual(stateData); + }); + }); + + describe('server side deserialization', () => { + it('should deserialize data', async () => { + const stateData = JSON.stringify([ + SerializationConstant.QRL_CHAR + 'entry_hooks.js#Root_component_arKLnchfR8k', + SerializationConstant.UNDEFINED_CHAR, + SerializationConstant.URL_CHAR + 'http://example.com', + ]); + const deserializedData = _deserialize(stateData) as unknown[]; + expect(isQrl(deserializedData[0])).toBeTruthy(); + expect(deserializedData[1]).toBeUndefined(); + expect(deserializedData[2] instanceof URL).toBeTruthy(); + }); + + it('should deserialize nested data', async () => { + const stateData = JSON.stringify([ + { + foo: SerializationConstant.URL_CHAR + 'http://example.com', + bar: [SerializationConstant.String_CHAR + 'abcd'], + }, + ]); + const [deserializedData] = _deserialize(stateData) as any[]; + expect(deserializedData.foo instanceof URL).toBeTruthy(); + expect(deserializedData.bar[0]).toEqual('abcd'); + }); + }); }); async function serializeDeserialize(...roots: any[]): Promise { diff --git a/packages/qwik/src/core/v2/shared/types.ts b/packages/qwik/src/core/v2/shared/types.ts index 167d602e672..b9e7ac1bdce 100644 --- a/packages/qwik/src/core/v2/shared/types.ts +++ b/packages/qwik/src/core/v2/shared/types.ts @@ -11,6 +11,12 @@ import type { SerializationContext } from './shared-serialization'; /// Temporary type left behind which needs to be fixed. export type fixMeAny = any; +export interface DeserializeContainer { + $getObjectById$: (id: number | string) => unknown; + element: HTMLElement | null; + getSyncFn: (id: number) => (...args: unknown[]) => unknown; +} + export interface Container2 { readonly $version$: string; readonly $scheduler$: Scheduler; @@ -54,6 +60,12 @@ export interface QElement2 extends HTMLElement { qDispatchEvent?: (event: Event, scope: QwikLoaderEventScope) => boolean; } +export type qWindow = Window & { + qwikevents: { + push: (...e: string[]) => void; + }; +}; + export type QwikLoaderEventScope = '-document' | '-window' | ''; export const isContainer2 = (container: any): container is Container2 => { diff --git a/packages/qwik/src/core/v2/shared/vnode-data-types.ts b/packages/qwik/src/core/v2/shared/vnode-data-types.ts index 40938e04781..f850b0ba94a 100644 --- a/packages/qwik/src/core/v2/shared/vnode-data-types.ts +++ b/packages/qwik/src/core/v2/shared/vnode-data-types.ts @@ -24,8 +24,8 @@ export const VNodeDataSeparator = { ADVANCE_16: /* ******** */ 37, // `%` is vNodeData separator skipping 8. ADVANCE_32_CH: /* **** */ `&`, // `&` is vNodeData separator skipping 16. ADVANCE_32: /* ******** */ 38, // `&` is vNodeData separator skipping 16. - ADVANCE_64_CH: /* **** */ '`', // '`'` is vNodeData separator skipping 32. - ADVANCE_64: /* ******** */ 39, // '`'` is vNodeData separator skipping 32. + ADVANCE_64_CH: /* **** */ `'`, // `'` is vNodeData separator skipping 32. + ADVANCE_64: /* ******** */ 39, // `'` is vNodeData separator skipping 32. ADVANCE_128_CH: /* *** */ `(`, // `(` is vNodeData separator skipping 64. ADVANCE_128: /* ******* */ 40, // `(` is vNodeData separator skipping 64. ADVANCE_256_CH: /* *** */ `)`, // `)` is vNodeData separator skipping 128. @@ -34,12 +34,12 @@ export const VNodeDataSeparator = { ADVANCE_512: /* ******* */ 42, // `*` is vNodeData separator skipping 256. ADVANCE_1024_CH: /* ** */ `+`, // `+` is vNodeData separator skipping 512. ADVANCE_1024: /* ****** */ 43, // `+` is vNodeData separator skipping 512. - ADVANCE_2048_CH: /* * */ '`', // '`'` is vNodeData separator skipping 1024. - ADVANCE_2048: /* ****** */ 44, // '`'` is vNodeData separator skipping 1024. - ADVANCE_4096_CH: /* * */ `.`, // `.` is vNodeData separator skipping 2048. - ADVANCE_4096: /* ****** */ 46, // `.` is vNodeData separator skipping 2048. - ADVANCE_8192_CH: /* * */ `/`, // `/` is vNodeData separator skipping 4096. - ADVANCE_9102: /* ****** */ 47, // `/` is vNodeData separator skipping 4096. + ADVANCE_2048_CH: /* * */ ',', // ',' is vNodeData separator skipping 1024. + ADVANCE_2048: /* ****** */ 44, // ',' is vNodeData separator skipping 1024. + ADVANCE_4096_CH: /* * */ `-`, // `-` is vNodeData separator skipping 2048. + ADVANCE_4096: /* ****** */ 45, // `-` is vNodeData separator skipping 2048. + ADVANCE_8192_CH: /* * */ `.`, // `.` is vNodeData separator skipping 4096. + ADVANCE_8192: /* ****** */ 46, // `.` is vNodeData separator skipping 4096. }; /** VNodeDataChar contains information about the VNodeData used for encoding props */ @@ -67,6 +67,8 @@ export const VNodeDataChar = { DON_T_USE_CHAR: '\\', CONTEXT: /* ************ */ 93, // `]` - `q:ctx' - Component context/props CONTEXT_CHAR: /* **** */ ']', + SEQ_IDX: /* ************ */ 94, // `^` - `q:seqIdx' - Sequential scope id + SEQ_IDX_CHAR: /* **** */ '^', SEPARATOR: /* ********* */ 124, // `|` - Separator char to encode any key/value pairs. SEPARATOR_CHAR: /* ** */ '|', SLOT: /* ************** */ 126, // `~` - `q:slot' - Slot name diff --git a/packages/qwik/src/core/v2/signal/v2-signal.public.ts b/packages/qwik/src/core/v2/signal/v2-signal.public.ts index f21f4b7736e..fa490c6be0e 100644 --- a/packages/qwik/src/core/v2/signal/v2-signal.public.ts +++ b/packages/qwik/src/core/v2/signal/v2-signal.public.ts @@ -7,16 +7,19 @@ import { export { isSignal2 as isSignal } from './v2-signal'; +/** @public */ export interface ReadonlySignal2 { readonly untrackedValue: T; readonly value: T; } +/** @public */ export interface Signal2 extends ReadonlySignal2 { untrackedValue: T; value: T; } +/** @public */ export interface ComputedSignal2 extends ReadonlySignal2 { /** * Use this to force recalculation and running subscribers, for example when the calculated value @@ -25,13 +28,15 @@ export interface ComputedSignal2 extends ReadonlySignal2 { force(): void; } - +/** @public */ export const createSignal2: { (): Signal2; (value: T): Signal2; } = _createSignal2; +/** @public */ export const createComputed2Qrl: (qrl: QRL<() => T>) => ComputedSignal2 = _createComputedSignal2; +/** @public */ export const createComputed2$ = /*#__PURE__*/ implicit$FirstArg(createComputed2Qrl); diff --git a/packages/qwik/src/core/v2/signal/v2-signal.ts b/packages/qwik/src/core/v2/signal/v2-signal.ts index 4c56d17ea3f..9ee6cfc1b27 100644 --- a/packages/qwik/src/core/v2/signal/v2-signal.ts +++ b/packages/qwik/src/core/v2/signal/v2-signal.ts @@ -46,6 +46,11 @@ export const createSignal2 = (value?: any) => { }; export const createComputedSignal2 = (qrl: QRL<() => T>) => { + throwIfQRLNotResolved(qrl); + return new ComputedSignal2(null, qrl as QRLInternal<() => T>); +}; + +export const throwIfQRLNotResolved = (qrl: QRL<() => T>) => { const resolved = qrl.resolved; if (!resolved) { // When we are creating a signal using a use method, we need to ensure @@ -55,7 +60,6 @@ export const createComputedSignal2 = (qrl: QRL<() => T>) => { // using useMethod) it is OK to not resolve it until the graph is marked as dirty. throw qrl.resolve(); } - return new ComputedSignal2(null, qrl as QRLInternal<() => T>); }; /** @public */ @@ -227,8 +231,6 @@ export const ensureContainsEffect = (array: EffectSubscriptions[], effect: Effec return; } } - console.log('array', array); - console.log('array.push', array.push); array.push(effect); }; @@ -245,16 +247,22 @@ export const triggerEffects = ( if (isTask(effect)) { effect.$flags$ |= TaskFlags.DIRTY; DEBUG && log('schedule.effect.task', pad('\n' + String(effect), ' ')); - container.$scheduler$( - effect.$flags$ & TaskFlags.VISIBLE_TASK ? ChoreType.VISIBLE : ChoreType.TASK, - effectSubscriptions as fixMeAny - ); + let choreType = ChoreType.TASK; + if (effect.$flags$ & TaskFlags.VISIBLE_TASK) { + choreType = ChoreType.VISIBLE; + } else if (effect.$flags$ & TaskFlags.RESOURCE) { + choreType = ChoreType.RESOURCE; + } + container.$scheduler$(choreType, effect); } else if (effect instanceof Signal2) { // we don't schedule ComputedSignal/DerivedSignal directly, instead we invalidate it and // and schedule the signals effects (recursively) if (effect instanceof ComputedSignal2) { - // TODO(misko): ensure that the computed signal's QRL is resolved. - // If not resolved scheduled it to be resolved. + // Ensure that the computed signal's QRL is resolved. + // If not resolved schedule it to be resolved. + if (!effect.$computeQrl$.resolved) { + container.$scheduler$(ChoreType.QRL_RESOLVE, null, effect.$computeQrl$); + } } (effect as ComputedSignal2 | DerivedSignal2).$invalid$ = true; const previousSignal = signal; @@ -302,8 +310,7 @@ export class ComputedSignal2 extends Signal2 { // we need the old value to know if effects need running after computation $invalid$: boolean = true; - constructor(container: Container2 | null, computeTask: QRLInternal<() => T> | null) { - assertDefined(computeTask, 'compute QRL must be provided'); + constructor(container: Container2 | null, computeTask: QRLInternal<() => T>) { // The value is used for comparison when signals trigger, which can only happen // when it was calculated before. Therefore we can pass whatever we like. super(container, NEEDS_COMPUTATION); diff --git a/packages/qwik/src/core/v2/signal/v2-store.ts b/packages/qwik/src/core/v2/signal/v2-store.ts index 50c193ce7e6..e4696d777c6 100644 --- a/packages/qwik/src/core/v2/signal/v2-store.ts +++ b/packages/qwik/src/core/v2/signal/v2-store.ts @@ -1,7 +1,7 @@ import { pad, qwikDebugToString } from '../../debug'; import { assertDefined, assertTrue } from '../../error/assert'; import { tryGetInvokeContext } from '../../use/use-core'; -import { isObject, isSerializableObject } from '../../util/types'; +import { isSerializableObject } from '../../util/types'; import type { VNode } from '../client/types'; import type { Container2, fixMeAny } from '../shared/types'; import { @@ -12,7 +12,7 @@ import { type EffectSubscriptions, } from './v2-signal'; -const DEBUG = true; +const DEBUG = false; // eslint-disable-next-line no-console const log = (...args: any[]) => console.log('STORE', ...args.map(qwikDebugToString)); @@ -36,7 +36,7 @@ let _lastHandler: undefined | StoreHandler; export const getStoreHandler2 = (value: T): StoreHandler | null => { _lastHandler = undefined as any; return typeof value === 'object' && value && STORE in value // this implicitly sets the `_lastHandler` as a side effect. - ? (_lastHandler!) + ? _lastHandler! : null; }; @@ -53,11 +53,12 @@ export const isStore2 = (value: T): value is Store2 => { return value instanceof Store; }; -export function createStore2(container: Container2 | null | undefined, obj: T & Record, flags: Store2Flags) { - return new Proxy( - new Store(), - new StoreHandler(obj, flags, container || null) - ) as Store2; +export function createStore2( + container: Container2 | null | undefined, + obj: T & Record, + flags: Store2Flags +) { + return new Proxy(new Store(), new StoreHandler(obj, flags, container || null)) as Store2; } export const getOrCreateStore2 = ( @@ -69,11 +70,11 @@ export const getOrCreateStore2 = ( let store: Store2 | undefined = storeWeakMap.get(obj) as Store2 | undefined; if (!store) { store = createStore2(container, obj, flags); - storeWeakMap.set(obj, store as any); + storeWeakMap.set(obj, store); } return store as Store2; } - return obj as any; + return obj as Store2; }; class Store { @@ -90,7 +91,7 @@ export class StoreHandler> implements Pro public $target$: T, public $flags$: Store2Flags, public $container$: Container2 | null - ) { } + ) {} toString() { const flags = []; @@ -158,6 +159,7 @@ export class StoreHandler> implements Pro const flags = this.$flags$; if (flags & Store2Flags.RECURSIVE && typeof value === 'object' && value !== null) { value = getOrCreateStore2(value, this.$flags$, this.$container$); + (target as Record)[p] = value; } return value; } @@ -168,7 +170,7 @@ export class StoreHandler> implements Pro if (value !== oldValue) { DEBUG && log('Signal.set', oldValue, '->', value, pad('\n' + this.toString(), ' ')); (target as any)[p] = value; - triggerEffects(this.$container$, this, this.$effects$?.[String(p)]); + triggerEffects(this.$container$, this, this.$effects$ ? this.$effects$[String(p)] : null); } return true; } diff --git a/packages/qwik/src/core/v2/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/v2/ssr/ssr-render-jsx.ts index ae694b1d0c8..9f5f2560af7 100644 --- a/packages/qwik/src/core/v2/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/v2/ssr/ssr-render-jsx.ts @@ -17,15 +17,7 @@ import { trackSignal2 } from '../../use/use-core'; import { isAsyncGenerator } from '../../util/async-generator'; import { EMPTY_ARRAY } from '../../util/flyweight'; import { throwErrorAndStop } from '../../util/log'; -import { - ELEMENT_KEY, - FLUSH_COMMENT, - QContainerAttr, - QContainerAttrEnd, - QDefaultSlot, - QScopedStyle, - QSlot, -} from '../../util/markers'; +import { ELEMENT_KEY, FLUSH_COMMENT, QDefaultSlot, QScopedStyle, QSlot } from '../../util/markers'; import { isPromise } from '../../util/promises'; import { isFunction, type ValueOrPromise } from '../../util/types'; import { @@ -36,10 +28,11 @@ import { } from '../shared/event-names'; import { addComponentStylePrefix, hasClassAttr, isClassAttr } from '../shared/scoped-styles'; import { qrlToString, type SerializationContext } from '../shared/shared-serialization'; -import { DEBUG_TYPE, QContainerValue, VirtualType, type fixMeAny } from '../shared/types'; +import { DEBUG_TYPE, VirtualType, type fixMeAny } from '../shared/types'; import { DerivedSignal2, EffectProperty, isSignal2 } from '../signal/v2-signal'; import { applyInlineComponent, applyQwikComponentBody } from './ssr-render-component'; import type { ISsrNode, SSRContainer, SsrAttrs } from './ssr-types'; +import { qInspector } from '../../util/qdev'; class SetScopedStyle { constructor(public $scopedStyle$: string | null) {} @@ -144,12 +137,13 @@ function processJSXNode( // TODO(mhevery): It is unclear to me why we need to serialize host for SignalDerived. // const host = ssr.getComponentFrame(0)!.componentNode as fixMeAny; enqueue(ssr.closeFragment); - enqueue(trackSignal2(() => (value.value as any), signalNode, EffectProperty.VNODE, ssr)); + enqueue(trackSignal2(() => value.value as any, signalNode, EffectProperty.VNODE, ssr)); } else if (isPromise(value)) { ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.Awaited] : EMPTY_ARRAY); enqueue(ssr.closeFragment); enqueue(value); enqueue(Promise); + enqueue(() => ssr.commentNode(FLUSH_COMMENT)); } else if (isAsyncGenerator(value)) { enqueue(async () => { for await (const chunk of value) { @@ -172,6 +166,8 @@ function processJSXNode( jsx.constProps['class'] = ''; } + appendQwikInspectorAttribute(jsx); + const innerHTML = ssr.openElement( type, varPropsToSsrAttrs( @@ -256,10 +252,9 @@ function processJSXNode( enqueue(value as StackValue); isPromise(value) && enqueue(Promise); } else if (type === SSRRaw) { - ssr.commentNode(QContainerAttr + '=' + QContainerValue.HTML); ssr.htmlNode(jsx.props.data as string); - ssr.commentNode(QContainerAttrEnd); } else if (isQwikComponent(type)) { + // prod: use new instance of an array for props, we always modify props for a component ssr.openComponent(isDev ? [DEBUG_TYPE, VirtualType.Component] : []); const host = ssr.getLastNode(); ssr.getComponentFrame(0)!.distributeChildrenIntoSlots(jsx.children, styleScoped); @@ -498,3 +493,17 @@ function getSlotName(host: ISsrNode, jsx: JSXNode, ssr: SSRContainer): string { } return (jsx.props.name as string) || QDefaultSlot; } + +function appendQwikInspectorAttribute(jsx: JSXNode) { + if (isDev && qInspector && jsx.dev && jsx.type !== 'head') { + const sanitizedFileName = jsx.dev.fileName?.replace(/\\/g, '/'); + const qwikInspectorAttr = 'data-qwik-inspector'; + if (sanitizedFileName && !(qwikInspectorAttr in jsx.props)) { + if (!jsx.constProps) { + jsx.constProps = {}; + } + jsx.constProps[qwikInspectorAttr] = + `${sanitizedFileName}:${jsx.dev.lineNumber}:${jsx.dev.columnNumber}`; + } + } +} diff --git a/packages/qwik/src/core/v2/ssr/ssr-types.ts b/packages/qwik/src/core/v2/ssr/ssr-types.ts index fdf95faf3a8..d4a99443419 100644 --- a/packages/qwik/src/core/v2/ssr/ssr-types.ts +++ b/packages/qwik/src/core/v2/ssr/ssr-types.ts @@ -56,9 +56,9 @@ export interface SSRContainer extends Container2 { closeContainer(): void; openElement( - tag: string, - attrs: SsrAttrs | null, - immutableAttrs?: SsrAttrs | null + elementName: string, + varAttrs: SsrAttrs | null, + constAttrs?: SsrAttrs | null ): string | undefined; closeElement(): ValueOrPromise; diff --git a/packages/qwik/src/core/v2/tests/component.spec.tsx b/packages/qwik/src/core/v2/tests/component.spec.tsx index 06ce0fe2124..5527a4caeb6 100644 --- a/packages/qwik/src/core/v2/tests/component.spec.tsx +++ b/packages/qwik/src/core/v2/tests/component.spec.tsx @@ -10,14 +10,14 @@ import { jsx, useComputed$, useVisibleTask$, + useSignal, + useStore, + type JSXOutput, } from '@builder.io/qwik'; -import { domRender, ssrRenderToDom } from '@builder.io/qwik/testing'; +import { domRender, ssrRenderToDom, trigger } from '@builder.io/qwik/testing'; import { describe, expect, it } from 'vitest'; -import { trigger } from '../../../testing/element-fixture'; +import { cleanupAttrs } from '../../../testing/element-fixture'; import { ErrorProvider } from '../../../testing/rendering.unit-util'; -import type { JSXOutput } from '../../render/jsx/types/jsx-node'; -import { useSignal } from '../../use/use-signal'; -import { useStore } from '../../use/use-store.public'; import { HTML_NS, MATH_NS, SVG_NS } from '../../util/markers'; import { delay } from '../../util/promises'; @@ -452,7 +452,7 @@ describe.each([

    ); - expect(document.querySelector('p')?.innerHTML).toEqual( + expect(cleanupAttrs(document.querySelector('p')?.innerHTML)).toEqual( 'Test124xx123XXX' ); }); @@ -493,18 +493,46 @@ describe.each([ it('should escape html tags', async () => { const Cmp = component$(() => { + const counter = useSignal(0); const b = ''; - return

    {JSON.stringify(b)}

    ; + return ( + + ); }); const { vNode, document } = await render(, { debug }); expect(vNode).toMatchVDOM( -

    {'""'}

    + +
    + ); + + await expect(document.querySelector('button')).toMatchDOM( + + ); + + await trigger(document.body, 'button', 'click'); + expect(vNode).toMatchVDOM( + + ); - expect(document.querySelector('p')).toMatchDOM(

    {'""'}

    ); + await expect(document.querySelector('button')).toMatchDOM( + + ); }); it('should render correctly with comment nodes', async () => { @@ -1563,7 +1591,7 @@ describe.each([ await delay(10); // console.log('vNode', String(vNode)); // console.log('>>>>', div.outerHTML); - expect(div.innerHTML).toEqual('(SomeErrorPOST)'); + expect(cleanupAttrs(div.innerHTML)).toEqual('(SomeErrorPOST)'); }); describe('regression', () => { diff --git a/packages/qwik/src/core/v2/tests/inline-component.spec.tsx b/packages/qwik/src/core/v2/tests/inline-component.spec.tsx new file mode 100644 index 00000000000..78bf1c2df76 --- /dev/null +++ b/packages/qwik/src/core/v2/tests/inline-component.spec.tsx @@ -0,0 +1,150 @@ +import { + Fragment as Component, + Fragment, + Fragment as InlineComponent, + component$, + useSignal, +} from '@builder.io/qwik'; +import { domRender, ssrRenderToDom, trigger } from '@builder.io/qwik/testing'; +import { describe, expect, it } from 'vitest'; + +const debug = false; //true; +Error.stackTraceLimit = 100; + +const MyComp = () => { + return ( + <> +

    Test

    +

    Lorem

    +

    ipsum

    +

    foo

    +

    bar

    + + ); +}; + +const InlineWrapper = () => { + return ; +}; + +describe.each([ + { render: ssrRenderToDom }, // + { render: domRender }, // +])('$render.name: inline component', ({ render }) => { + it('should render inline component', async () => { + const MyComp = () => { + return <>Hello World!; + }; + + const { vNode } = await render(, { debug }); + expect(vNode).toMatchVDOM( + <> + <>Hello World! + + ); + }); + + it('should render nested component', async () => { + const Child = (props: { name: string }) => { + return <>{props.name}; + }; + + const Parent = (props: { salutation: string; name: string }) => { + return ( + <> + {props.salutation} + + ); + }; + + const { vNode } = await render(, { + debug, + }); + expect(vNode).toMatchVDOM( + + + {'Hello'}{' '} + + World + + + + ); + }); + + it('should toggle component$ and inline wrapper', async () => { + const Test = component$(() => { + return
    Test
    ; + }); + const Wrapper = component$(() => { + const toggle = useSignal(true); + return ( + <> + + {toggle.value ? : } + + ); + }); + + const { vNode, document } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + <> + + +
    Test
    +
    + +
    + ); + await trigger(document.body, 'button', 'click'); + expect(vNode).toMatchVDOM( + + <> + + + + +

    Test

    +

    Lorem

    +

    ipsum

    +

    foo

    +

    bar

    +
    +
    +
    + +
    + ); + await trigger(document.body, 'button', 'click'); + expect(vNode).toMatchVDOM( + + <> + + +
    Test
    +
    + +
    + ); + await trigger(document.body, 'button', 'click'); + expect(vNode).toMatchVDOM( + + <> + + + + +

    Test

    +

    Lorem

    +

    ipsum

    +

    foo

    +

    bar

    +
    +
    +
    + +
    + ); + }); +}); diff --git a/packages/qwik/src/core/v2/tests/projection.spec.tsx b/packages/qwik/src/core/v2/tests/projection.spec.tsx index 43b573842a5..01bd8cca26b 100644 --- a/packages/qwik/src/core/v2/tests/projection.spec.tsx +++ b/packages/qwik/src/core/v2/tests/projection.spec.tsx @@ -21,6 +21,7 @@ import { } from '@builder.io/qwik'; import { vnode_getNextSibling } from '../client/vnode'; import { HTML_NS, SVG_NS } from '../../util/markers'; +import { cleanupAttrs } from 'packages/qwik/src/testing/element-fixture'; const DEBUG = false; @@ -41,18 +42,18 @@ describe.each([ { render: ssrRenderToDom }, // { render: domRender }, // ])('$render.name: projection', ({ render }) => { - it.only('should render basic projection', async () => { + it('should render basic projection', async () => { const Child = component$(() => { return (
    - misko + misko
    ); }); const Parent = component$(() => { return ( - parent-content + parent-content ); }); @@ -61,7 +62,9 @@ describe.each([
    - parent-content + + parent-content +
    @@ -398,7 +401,7 @@ describe.each([
    - {''} +
    @@ -570,7 +573,6 @@ describe.each([ - {''} @@ -578,7 +580,6 @@ describe.each([ - {''} @@ -588,6 +589,143 @@ describe.each([ ); }); + it('should toggle named slot to nothing', async () => { + const Projector = component$((props: { state: any; id: string }) => { + return ( +
    + + + +
    + ); + }); + + const Parent = component$(() => { + const state = useStore({ + toggle: true, + count: 0, + }); + return ( + <> + + {state.toggle && <>DEFAULT {state.count}} + + + + {state.toggle && START {state.count}} + {state.toggle && END {state.count}} + + + + + ); + }); + + const { vNode, document } = await render(, { debug: DEBUG }); + expect(vNode).toMatchVDOM( + + + +
    + {render === ssrRenderToDom ? '' : null} + + + {'DEFAULT '} + {'0'} + + + {render === ssrRenderToDom ? '' : null} +
    +
    + +
    + + + {'START '} + {'0'} + + + {render === ssrRenderToDom ? '' : null} + + + {'END '} + {'0'} + + +
    +
    + + +
    +
    + ); + + await trigger(document.body, '#toggle', 'click'); + expect(vNode).toMatchVDOM( + + + +
    + {render === ssrRenderToDom ? '' : null} + + {render === ssrRenderToDom ? '' : null} +
    +
    + +
    + + {render === ssrRenderToDom ? '' : null} + +
    +
    + + +
    +
    + ); + + await trigger(document.body, '#count', 'click'); + await trigger(document.body, '#toggle', 'click'); + + expect(vNode).toMatchVDOM( + + + +
    + {render === ssrRenderToDom ? '' : null} + + + {'DEFAULT '} + {'1'} + + + {render === ssrRenderToDom ? '' : null} +
    +
    + +
    + + + {'START '} + {'1'} + + + {render === ssrRenderToDom ? '' : null} + + + {'END '} + {'1'} + + +
    +
    + + +
    +
    + ); + }); + it('should render to named slot in nested named slots', async () => { const NestedSlotCmp = component$(() => { return ( @@ -1111,8 +1249,7 @@ describe.each([ await trigger(document.body, '#reload', 'click'); }); - // TODO(slot): fix this test - it.skip('should not go into an infinity loop because of removing nodes from q:template', async () => { + it('should not go into an infinity loop because of removing nodes from q:template', async () => { const Projector = component$(() => { return (
    @@ -1242,7 +1379,7 @@ describe.each([ style="width:24px;height:24px" xmlns="http://www.w3.org/2000/svg" > - {''} + @@ -1557,7 +1694,7 @@ describe.each([ , { debug: DEBUG } ); - expect(removeKeyAttrs(document.querySelector('div')?.innerHTML || '')).toContain( + expect(cleanupAttrs(document.querySelector('div')?.innerHTML || '')).toContain( '

    CHILDDYNAMIC' ); await trigger(document.body, 'button', 'click'); @@ -1572,7 +1709,7 @@ describe.each([
    ); - expect(removeKeyAttrs(document.querySelector('div')?.innerHTML || '')).not.toContain( + expect(cleanupAttrs(document.querySelector('div')?.innerHTML || '')).not.toContain( 'CHILDDYNAMIC' ); await trigger(document.body, 'button', 'click'); @@ -1592,7 +1729,7 @@ describe.each([ ); - expect(removeKeyAttrs(document.querySelector('div')?.innerHTML || '')).toContain( + expect(cleanupAttrs(document.querySelector('div')?.innerHTML || '')).toContain( '

    CHILDDYNAMIC' ); }); @@ -1735,8 +1872,7 @@ describe.each([ ); }); - // TODO(slot): fix this test - it.skip('#2688 - case 2', async () => { + it('#2688 - case 2', async () => { const Switch = component$((props: { name: string }) => { return ; }); @@ -2096,7 +2232,3 @@ describe.each([ }); }); }); - -function removeKeyAttrs(innerHTML: string): any { - return innerHTML.replaceAll(/ q:key="[^"]+"/g, ''); -} diff --git a/packages/qwik/src/core/v2/tests/ref.spec.tsx b/packages/qwik/src/core/v2/tests/ref.spec.tsx new file mode 100644 index 00000000000..9c01565ae34 --- /dev/null +++ b/packages/qwik/src/core/v2/tests/ref.spec.tsx @@ -0,0 +1,56 @@ +import { component$, useSignal, useVisibleTask$, Fragment as Component } from '@builder.io/qwik'; +import { domRender, ssrRenderToDom, trigger } from '@builder.io/qwik/testing'; +import { describe, expect, it } from 'vitest'; + +const debug = false; //true; +Error.stackTraceLimit = 100; + +describe.each([ + { render: ssrRenderToDom }, // + { render: domRender }, // +])('$render.name: ref', ({ render }) => { + describe('useVisibleTask$', () => { + it('should handle ref prop', async () => { + const Cmp = component$(() => { + const v = useSignal(); + useVisibleTask$(() => { + v.value!.textContent = 'Abcd'; + }); + return

    Hello Qwik

    ; + }); + + const { document } = await render(, { debug }); + + if (render === ssrRenderToDom) { + await trigger(document.body, 'p', 'qvisible'); + } + + await expect(document.querySelector('p')).toMatchDOM(

    Abcd

    ); + }); + }); + + it('should execute function', async () => { + (global as any).logs = [] as string[]; + const Cmp = component$(() => { + return ( +
    { + (global as any).logs.push('ref function', element); + }} + >
    + ); + }); + + const { vNode } = await render(, { debug }); + + expect(vNode).toMatchVDOM( + +
    +
    + ); + + expect((global as any).logs[0]).toEqual('ref function'); + expect((global as any).logs[1]).toBeDefined(); + (global as any).logs = undefined; + }); +}); diff --git a/packages/qwik/src/core/v2/tests/render-api.spec.tsx b/packages/qwik/src/core/v2/tests/render-api.spec.tsx index 8450bb5b451..d41d4376234 100644 --- a/packages/qwik/src/core/v2/tests/render-api.spec.tsx +++ b/packages/qwik/src/core/v2/tests/render-api.spec.tsx @@ -35,27 +35,27 @@ import { renderToStream2, renderToString2 } from '../../../server/v2-ssr-render2 import { _fnSignal } from '../../internal'; import { render2 } from '../client/dom-render'; import { vnode_getFirstChild } from '../client/vnode'; +import { cleanupAttrs } from 'packages/qwik/src/testing/element-fixture'; vi.hoisted(() => { vi.stubGlobal('QWIK_LOADER_DEFAULT_MINIFIED', 'min'); vi.stubGlobal('QWIK_LOADER_DEFAULT_DEBUG', 'debug'); }); +const mapping = { + click: 'click.js', + s_counter: 's_counter.js', + s_click: 's_click.js', +}; + const defaultManifest: QwikManifest = { manifestHash: 'manifest-hash', symbols: {}, bundles: {}, - mapping: {}, + mapping, version: '1', }; -const defaultCounterManifest: QwikManifest = { - ...defaultManifest, - mapping: { - click: 'click.js', - }, -}; - const ManyEventsComponent = component$(() => { useOn( 'focus', @@ -265,6 +265,30 @@ describe('render api', () => { expect(timing.render).toBeGreaterThan(0); expect(timing.snapshot).toBeGreaterThan(0); }); + + it('should escape invalid characters', async () => { + const Cmp = componentQrl( + inlinedQrl(() => { + const obj = { + a: '123', + b: ''); + expect(cleanupAttrs(result.html)).toContain(''); }); it('should emit qwik loader without debug mode', async () => { @@ -833,7 +855,7 @@ describe('render api', () => { containerTagName: 'div', debug: false, }); - expect(result.html).toContain(''); + expect(cleanupAttrs(result.html)).toContain(''); }); }); describe('snapshotResult', () => { diff --git a/packages/qwik/src/core/v2/tests/ssr-render.spec.tsx b/packages/qwik/src/core/v2/tests/ssr-render.spec.tsx index 0533a50f8f8..4c4d62ba8be 100644 --- a/packages/qwik/src/core/v2/tests/ssr-render.spec.tsx +++ b/packages/qwik/src/core/v2/tests/ssr-render.spec.tsx @@ -56,7 +56,9 @@ describe('v2 ssr render', () => { return (
    + + a @@ -176,8 +178,10 @@ describe('v2 ssr render', () => {
    ); // we should not stream the comment nodes of the SSRStreamBlock - expect(document.querySelector('#stream-block')?.innerHTML).toEqual( - '
    stream content
    ' + expect(document.querySelector('#stream-block')).toMatchDOM( +
    +
    stream content
    +
    ); }); diff --git a/packages/qwik/src/core/v2/tests/use-computed.spec.tsx b/packages/qwik/src/core/v2/tests/use-computed.spec.tsx index 4d5d1b09547..c4542f6bfd8 100644 --- a/packages/qwik/src/core/v2/tests/use-computed.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-computed.spec.tsx @@ -48,6 +48,25 @@ describe.each([ ); }); + it('should render correctly with falsy value', async () => { + const Cmp = component$((props: { initial: number }) => { + const count = useSignal(props.initial); + const doubleCount = useComputed$(() => count.value * 2); + return ( +
    + Double count: {doubleCount.value}! {count.value} +
    + ); + }); + const { vNode } = await render(, { debug }); + expect(vNode).toMatchVDOM( + <> +
    + Double count: {'0'}! {'0'} +
    + + ); + }); it('should update value based on signal', async () => { const DoubleCounter = component$((props: { initial: number }) => { const count = useSignal(props.initial); @@ -223,18 +242,18 @@ describe.each([ ); }); - it.skip('#3294 - improvement(after v2): should lazily evaluate the function with useSignal', async () => { - let useComputedCount = 0; + it('#3294 - should lazily evaluate the function with useSignal', async () => { + (global as any).useComputedCount = 0; const Issue3294 = component$(() => { const firstName = useSignal('Misko'); const lastName = useSignal('Hevery'); const execFirstUseComputed = useSignal(true); const firstUseComputed = useComputed$(() => { - useComputedCount++; + (global as any).useComputedCount++; return lastName.value + ' ' + firstName.value; }); const secondUseComputed = useComputed$(() => { - useComputedCount++; + (global as any).useComputedCount++; return firstName.value + ' ' + lastName.value; }); return ( @@ -252,24 +271,26 @@ describe.each([ expect(vNode).toMatchVDOM( <>
    - Hevery Misko + + Hevery Misko +
    ); - expect(useComputedCount).toBe(1); + expect((global as any).useComputedCount).toBe(1); }); - it.skip('#3294 - improvement(after v2): should lazily evaluate the function with store', async () => { - let useComputedCount = 0; + it('#3294 - should lazily evaluate the function with store', async () => { + (global as any).useComputedCount = 0; const Issue3294 = component$(() => { const store = useStore({ firstName: 'Misko', lastName: 'Hevery' }); const execFirstUseComputed = useSignal(true); const firstUseComputed = useComputed$(() => { - useComputedCount++; + (global as any).useComputedCount++; return store.lastName + ' ' + store.firstName; }); const secondUseComputed = useComputed$(() => { - useComputedCount++; + (global as any).useComputedCount++; return store.firstName + ' ' + store.lastName; }); return ( @@ -287,11 +308,13 @@ describe.each([ expect(vNode).toMatchVDOM( <>
    - Hevery Misko + + Hevery Misko +
    ); - expect(useComputedCount).toBe(1); + expect((global as any).useComputedCount).toBe(1); }); }); }); diff --git a/packages/qwik/src/core/v2/tests/use-context.spec.tsx b/packages/qwik/src/core/v2/tests/use-context.spec.tsx index 16bd3a7399e..cc7fb41a6ff 100644 --- a/packages/qwik/src/core/v2/tests/use-context.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-context.spec.tsx @@ -13,7 +13,6 @@ import { component$, noSerialize, type NoSerialize, - type Signal, getPlatform, setPlatform, _getDomContainer, @@ -28,6 +27,7 @@ import { } from '@builder.io/qwik/testing'; import '../../../testing/vdom-diff.unit-util'; import { renderToString2 } from 'packages/qwik/src/server/v2-ssr-render2'; +import type { Signal2 } from '../signal/v2-signal.public'; const debug = false; //true; Error.stackTraceLimit = 100; @@ -266,7 +266,7 @@ describe.each([ describe('html wrapper', () => { it('should provide and retrieve a context in client', async () => { - const contextId = createContextId>>('myTest'); + const contextId = createContextId>>('myTest'); const Consumer = component$(() => { const ctxValue = useContext(contextId); return {ctxValue.value?.value}; diff --git a/packages/qwik/src/core/v2/tests/use-on.spec.tsx b/packages/qwik/src/core/v2/tests/use-on.spec.tsx index 30280aaab60..dc8ea48a865 100644 --- a/packages/qwik/src/core/v2/tests/use-on.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-on.spec.tsx @@ -1,13 +1,16 @@ import { $, Fragment as Component, + Fragment, Fragment as Signal, + Fragment as Awaited, component$, useOn, useOnDocument, useOnWindow, useSignal, useVisibleTask$, + useTask$, } from '@builder.io/qwik'; import { describe, expect, it } from 'vitest'; import { trigger } from '../../../testing/element-fixture'; @@ -225,6 +228,35 @@ describe.each([ ); }); + it('should work with empty component', async () => { + const Counter = component$((props: { initial: number }) => { + const count = useSignal(props.initial); + useOnDocument( + 'click', + $(() => count.value++) + ); + return <>Count: {count.value}!; + }); + + const { vNode, container } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + Count: {'123'}! + + + ); + + await trigger(container.element, 'script', ':document:click'); + expect(vNode).toMatchVDOM( + + + Count: {'124'}! + + + ); + }); + it('should update value with mixed listeners', async () => { const Counter = component$((props: { initial: number }) => { const count = useSignal(props.initial); @@ -292,6 +324,35 @@ describe.each([ ); }); + it('should work with empty component', async () => { + const Counter = component$((props: { initial: number }) => { + const count = useSignal(props.initial); + useOnWindow( + 'click', + $(() => count.value++) + ); + return <>Count: {count.value}!; + }); + + const { vNode, container } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + Count: {'123'}! + + + ); + + await trigger(container.element, 'script', ':window:click'); + expect(vNode).toMatchVDOM( + + + Count: {'124'}! + + + ); + }); + it('should update value for window event on element and useOnWindow', async () => { const Counter = component$((props: { initial: number }) => { const count = useSignal(props.initial); @@ -489,4 +550,102 @@ describe.each([ ); }); + + it('should not add script node in empty components for specific events', async () => { + const Cmp = component$(() => { + const signal = useSignal('empty'); + useOn( + 'click', + $(() => { + signal.value = 'run'; + }) + ); + return <>{signal.value}; + }); + const { vNode, document } = await render(, { debug }); + await trigger(document.body, 'script', 'click'); + expect(vNode).toMatchVDOM( + + + {'empty'} + + + ); + }); + + it('should add event to element returned by promise', async () => { + const Cmp = component$(() => { + const signal = useSignal('empty'); + useOn( + 'click', + $(() => { + signal.value = 'run'; + }) + ); + return <>{Promise.resolve(
    {signal.value}
    )}; + }); + const { vNode, document } = await render(, { debug }); + await trigger(document.body, 'div', 'click'); + expect(vNode).toMatchVDOM( + + + +
    + {'run'} +
    +
    +
    +
    + ); + }); + + it('should add event to element returned by signal', async () => { + const Cmp = component$(() => { + const signal = useSignal('empty'); + const jsx = useSignal(
    {signal.value}
    ); + useOn( + 'click', + $(() => { + signal.value = 'run'; + }) + ); + return <>{jsx.value}; + }); + const { vNode, document } = await render(, { debug }); + await trigger(document.body, 'div', 'click'); + expect(vNode).toMatchVDOM( + + + +
    + {'run'} +
    +
    +
    +
    + ); + }); + + it('should add only one event', async () => { + const Cmp = component$(() => { + const signal = useSignal(0); + useOn( + 'click', + $(() => { + signal.value++; + }) + ); + + useTask$(async ({ track }) => { + track(() => signal); + // rerender component twice + await Promise.resolve(); + await Promise.resolve(); + }); + return <>{Promise.resolve(
    {signal.value}
    )}; + }); + const { document } = await render(, { debug }); + await trigger(document.body, 'div', 'click'); + await expect(document.querySelector('div')).toMatchDOM(
    1
    ); + }); }); diff --git a/packages/qwik/src/core/v2/tests/use-resource.spec.tsx b/packages/qwik/src/core/v2/tests/use-resource.spec.tsx index e4650bc83df..cc6f5f24286 100644 --- a/packages/qwik/src/core/v2/tests/use-resource.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-resource.spec.tsx @@ -3,16 +3,15 @@ import { Fragment as Component, Fragment, Fragment as InlineComponent, + Fragment as Signal, Resource, component$, useResource$, useSignal, } from '@builder.io/qwik'; import { describe, expect, it } from 'vitest'; -import { trigger } from '../../../testing/element-fixture'; -import { domRender, ssrRenderToDom } from '../../../testing/rendering.unit-util'; import '../../../testing/vdom-diff.unit-util'; -import { delay } from '../../util/promises'; +import { getTestPlatform, trigger, domRender, ssrRenderToDom } from '@builder.io/qwik/testing'; const debug = false; //true; Error.stackTraceLimit = 100; @@ -45,13 +44,12 @@ describe.each([ ); }); + it('should update resource task', async () => { const TestCmp = component$(() => { const count = useSignal(0); const rsrc = useResource$(async ({ track }) => { - const value = track(() => count.value); - await delay(5); - return value; + return track(() => count.value); }); return ( + + ); + }); + + it('should show loading state', async () => { + (global as any).delay = () => new Promise((res) => ((global as any).delay.resolve = res)); + const ResourceCmp = component$(() => { + const count = useSignal(0); + const rsrc = useResource$(async ({ track }) => { + const value = track(() => count.value); + if (count.value === 1) { + await (global as any).delay(); + } + return value; + }); + return ( + + ); + }); + const { vNode, container } = await render(, { debug }); + + expect(vNode).toMatchVDOM( + + + + ); + + await trigger(container.element, 'button', 'click'); + expect(vNode).toMatchVDOM( + + + + ); + await (global as any).delay.resolve(); + await getTestPlatform().flush(); + + expect(vNode).toMatchVDOM( + + ); + (global as any).delay = undefined; + }); + + it('should immediately increment button count', async () => { + (global as any).delay = () => new Promise((res) => ((global as any).delay.resolve = res)); + const ResourceCmp = component$(() => { + const count = useSignal(0); + const resource = useResource$(async ({ track }) => { + track(count); + const value = count.value; + if (count.value >= 1) { + await (global as any).delay(); + } + return value; + }); + + return ( + <> + +
    {data}
    } /> + + ); + }); + + const { vNode, container } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + + + + +
    0
    +
    +
    +
    +
    +
    + ); + await trigger(container.element, 'button', 'click'); + + expect(vNode).toMatchVDOM( + + + + + + +
    0
    +
    +
    +
    +
    +
    + ); + await (global as any).delay.resolve(); + await getTestPlatform().flush(); + + expect(vNode).toMatchVDOM( + + + + + + +
    1
    +
    +
    +
    +
    +
    + ); + (global as any).delay = undefined; + }); + + it('should handle multiple the same resource tasks', async () => { + (global as any).delay = () => new Promise((res) => ((global as any).delay.resolve = res)); + + const ResourceRaceCondition = component$(() => { + const count = useSignal(0); + const resource = useResource$(async ({ track }) => { + track(count); + const value = count.value; + if (count.value === 1) { + await (global as any).delay(); + } + return value; + }); + + return ( + <> + +
    {data}
    } /> + + ); + }); + + const { vNode, container } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + + + + +
    0
    +
    +
    +
    +
    +
    + ); + // double click + await trigger(container.element, 'button', 'click'); + await trigger(container.element, 'button', 'click'); + await (global as any).delay.resolve(); + await getTestPlatform().flush(); + + expect(vNode).toMatchVDOM( + + + + + + +
    2
    +
    +
    +
    +
    +
    + ); }); }); diff --git a/packages/qwik/src/core/v2/tests/use-signal.spec.tsx b/packages/qwik/src/core/v2/tests/use-signal.spec.tsx index e917f4a90d4..2d62100c00c 100644 --- a/packages/qwik/src/core/v2/tests/use-signal.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-signal.spec.tsx @@ -226,6 +226,43 @@ describe.each([ ); }); + + // TODO: should be fixed after signals v2 is implemented + it.skip('should not execute signal when not used', async () => { + const Cmp = component$(() => { + const data = useSignal<{ price: number } | null>({ price: 100 }); + return ( +
    + + {data.value == null && not found} + {data.value != null && {data.value.price}} +
    + ); + }); + const { vNode, document } = await render(, { debug }); + expect(vNode).toMatchVDOM( + +
    + + {''} + + 100 + +
    +
    + ); + await trigger(document.body, 'button', 'click'); + expect(vNode).toMatchVDOM( + +
    + + not found + {''} +
    +
    + ); + }); + describe('derived', () => { it('should update value directly in DOM', async () => { const log: string[] = []; diff --git a/packages/qwik/src/core/v2/tests/use-store.spec.tsx b/packages/qwik/src/core/v2/tests/use-store.spec.tsx index faa259eedfa..ca0c412622c 100644 --- a/packages/qwik/src/core/v2/tests/use-store.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-store.spec.tsx @@ -1,20 +1,22 @@ import { Fragment as Component, Fragment, Fragment as Signal, useTask$ } from '@builder.io/qwik'; import { describe, expect, it, vi } from 'vitest'; -import { advanceToNextTimerAndFlush, trigger } from '../../../testing/element-fixture'; -import { domRender, ssrRenderToDom } from '../../../testing/rendering.unit-util'; -import '../../../testing/vdom-diff.unit-util'; -import { component$ } from '@builder.io/qwik'; -import type { Signal as SignalType } from '../../state/signal'; -import { untrack } from '../../use/use-core'; -import { useSignal } from '../../use/use-signal'; -import { useStore } from '../../use/use-store.public'; - -const debug = true; //true; +import { advanceToNextTimerAndFlush } from '../../../testing/element-fixture'; +import { domRender, ssrRenderToDom, trigger } from '@builder.io/qwik/testing'; +import { + component$, + type Signal as SignalType, + untrack, + useSignal, + useStore, + useTask$, +} from '@builder.io/qwik'; + +const debug = false; //true; Error.stackTraceLimit = 100; describe.each([ { render: ssrRenderToDom }, // - // { render: domRender }, // + { render: domRender }, // ])('$render.name: useStore', ({ render }) => { it('should render value', async () => { const Cmp = component$(() => { @@ -60,7 +62,7 @@ describe.each([ ); }); - it.only('should update deep value', async () => { + it('should update deep value', async () => { const Counter = component$(() => { const count = useStore({ obj: { count: 123 } }); return ; @@ -189,9 +191,7 @@ describe.each([ const count = useStore({ jsx: 'initial' }); log.push('Counter: ' + untrack(() => count.jsx)); return ( - ); @@ -283,6 +283,187 @@ describe.each([ }); }); + describe('SerializationConstant at the start', () => { + it('should set the value with SerializationConstant at the start for initial empty value', async () => { + const DataCmp = component$(() => { + const data = useStore({ logs: '' }); + return ; + }); + + const { vNode, container } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + + ); + await trigger(container.element, 'button', 'click'); + expect(vNode).toMatchVDOM( + + + + ); + }); + + it('should set the value with SerializationConstant at the start', async () => { + const DataCmp = component$(() => { + const data = useStore({ logs: '\n abcd' }); + return ; + }); + + const { vNode, container } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + + ); + await trigger(container.element, 'button', 'click'); + expect(vNode).toMatchVDOM( + + + + ); + }); + + it('should update the value with SerializationConstant at the start', async () => { + const DataCmp = component$(() => { + const data = useStore({ logs: '\n abcd' }); + return ; + }); + + const { vNode, container } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + + ); + await trigger(container.element, 'button', 'click'); + expect(vNode).toMatchVDOM( + + + + ); + }); + + it('should push the value with SerializationConstant at the start to array', async () => { + const DataCmp = component$(() => { + const data = useStore({ logs: ['\n abcd'] }); + return ( + + ); + }); + + const { vNode, container } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + + ); + await trigger(container.element, 'button', 'click'); + expect(vNode).toMatchVDOM( + + + + ); + }); + }); + + it('should deep watch store', async () => { + const Cmp = component$(() => { + const store = useStore({ + nested: { + fields: { are: 'also tracked' }, + }, + list: ['Item 1'], + }); + + return ( + <> +

    {store.nested.fields.are}

    + + +
      + {store.list.map((item, key) => ( +
    • {item}
    • + ))} +
    + + ); + }); + + const { vNode, document } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + +

    + also tracked +

    + + +
      +
    • Item 1
    • +
    +
    +
    + ); + await trigger(document.body, 'button#add-item', 'click'); + await trigger(document.body, 'button#add-item', 'click'); + expect(vNode).toMatchVDOM( + + +

    + also tracked +

    + + +
      +
    • Item 1
    • +
    • Item 1
    • +
    • Item 2
    • +
    +
    +
    + ); + }); + describe('regression', () => { it('#5597 - should update value', async () => { (globalThis as any).clicks = 0; @@ -460,8 +641,7 @@ describe.each([ ); }); - // TODO(optimizer-test): this is failing also in v1 - it.skip('#5017 - should update child nodes for direct array', async () => { + it('#5017 - should update child nodes for direct array', async () => { const Child = component$<{ columns: string }>(({ columns }) => { return
    Child: {columns}
    ; }); @@ -488,13 +668,13 @@ describe.each([
    {'Child: '} - {'INITIAL'} + {'INITIAL'}
    {'Child: '} - {'INITIAL'} + {'INITIAL'}
    @@ -508,13 +688,13 @@ describe.each([
    {'Child: '} - {'UPDATE'} + {'UPDATE'}
    {'Child: '} - {'UPDATE'} + {'UPDATE'}
    diff --git a/packages/qwik/src/core/v2/tests/use-styles-scoped.spec.tsx b/packages/qwik/src/core/v2/tests/use-styles-scoped.spec.tsx index 801e58e77c8..3ed3f11d036 100644 --- a/packages/qwik/src/core/v2/tests/use-styles-scoped.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-styles-scoped.spec.tsx @@ -8,7 +8,7 @@ import { createDocument } from '@builder.io/qwik-dom'; import { afterEach, describe, expect, it } from 'vitest'; import { useStore } from '../..'; import { renderToString2 } from '../../../server/v2-ssr-render2'; -import { trigger } from '../../../testing/element-fixture'; +import { cleanupAttrs, trigger } from '../../../testing/element-fixture'; import { domRender, ssrRenderToDom } from '../../../testing/rendering.unit-util'; import '../../../testing/vdom-diff.unit-util'; import { component$ } from '../../component/component.public'; @@ -123,7 +123,9 @@ describe.each([ ); const style = container.document.querySelector(QStyleSelector); - expect(style?.outerHTML).toEqual(``); + expect(cleanupAttrs(style?.outerHTML)).toEqual( + `` + ); }); it('should save styles when JSX deleted', async () => { @@ -154,7 +156,9 @@ describe.each([ ); const style = container.document.querySelector(QStyleSelector); - expect(style?.outerHTML).toEqual(``); + expect(cleanupAttrs(style?.outerHTML)).toEqual( + `` + ); }); it('style node should contain q:style attribute', async () => { @@ -252,7 +256,7 @@ describe.each([ ); const qStyles = container.document.querySelectorAll(QStyleSelector); expect(qStyles).toHaveLength(2); - expect(Array.from(qStyles).map((style) => style.outerHTML)).toEqual( + expect(Array.from(qStyles).map((style) => cleanupAttrs(style.outerHTML))).toEqual( expect.arrayContaining([ ``, ``, diff --git a/packages/qwik/src/core/v2/tests/use-styles.spec.tsx b/packages/qwik/src/core/v2/tests/use-styles.spec.tsx index 5e216923ce8..aed289cee7f 100644 --- a/packages/qwik/src/core/v2/tests/use-styles.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-styles.spec.tsx @@ -71,9 +71,8 @@ describe.each([ ); const style = container.document.querySelector(QStyleSelector); - expect(style?.outerHTML).toEqual( - `` - ); + const attrs = { 'q:style': (globalThis as any).rawStyleId }; + expect(style).toMatchDOM(); }); it('should save styles when JSX deleted', async () => { @@ -101,9 +100,8 @@ describe.each([ ); const style = container.document.querySelector(QStyleSelector); - expect(style?.outerHTML).toEqual( - `` - ); + const attrs = { 'q:style': (globalThis as any).rawStyleId }; + expect(style).toMatchDOM(); }); it('style node should contain q:style attribute', async () => { diff --git a/packages/qwik/src/core/v2/tests/use-task.spec.tsx b/packages/qwik/src/core/v2/tests/use-task.spec.tsx index fd8265b37d6..17d23924140 100644 --- a/packages/qwik/src/core/v2/tests/use-task.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-task.spec.tsx @@ -1,15 +1,18 @@ import { describe, expect, it } from 'vitest'; -import { trigger } from '../../../testing/element-fixture'; -import { getTestPlatform } from '../../../testing/platform'; -import { ErrorProvider, domRender, ssrRenderToDom } from '../../../testing/rendering.unit-util'; -import '../../../testing/vdom-diff.unit-util'; -import { component$ } from '../../component/component.public'; -import { Fragment as Component, Fragment, Fragment as Signal } from '../../render/jsx/jsx-runtime'; -import { SignalDerived, type Signal as SignalType } from '../../state/signal'; -import { useSignal } from '../../use/use-signal'; -import { useStore } from '../../use/use-store.public'; -import { useTask$ } from '../../use/use-task-dollar'; +import { ErrorProvider } from '../../../testing/rendering.unit-util'; +import { domRender, ssrRenderToDom, getTestPlatform, trigger } from '@builder.io/qwik/testing'; import { delay } from '../../util/promises'; +import { + useSignal, + useStore, + useTask$, + Fragment as Component, + Fragment, + Fragment as Signal, + component$, + type Signal as SignalType, +} from '@builder.io/qwik'; +import { DerivedSignal2 } from '../signal/v2-signal'; const debug = false; //true; Error.stackTraceLimit = 100; @@ -180,7 +183,12 @@ describe.each([ it('should rerun on track derived signal', async () => { const Counter = component$(() => { const countRaw = useStore({ count: 10 }); - const count = new SignalDerived((o: any, prop: string) => o[prop], [countRaw, 'count']); + const count = new DerivedSignal2( + null, + (o: any, prop: string) => o[prop], + [countRaw, 'count'], + null + ); const double = useSignal(0); useTask$(({ track }) => { double.value = 2 * track(() => count.value); @@ -233,6 +241,63 @@ describe.each([ ); }); + + it('should unsubscribe from removed component', async () => { + (global as any).logs = [] as string[]; + + const ToggleChild = component$((props: { name: string; count: number }) => { + useTask$(({ track }) => { + const count = track(() => props.count); + const logText = `Child of "${props.name}" (${count})`; + (global as any).logs.push(logText); + }); + + return ( +
    +

    Toggle {props.name}

    +
    + ); + }); + + const Toggle = component$(() => { + const store = useStore({ + count: 0, + cond: false, + }); + return ( +
    + +
    + {!store.cond ? ( + + ) : ( + + )} + +
    +
    + ); + }); + + const { document } = await render(, { debug }); + + await trigger(document.body, '#increment', 'click'); + await trigger(document.body, '#toggle', 'click'); + await trigger(document.body, '#increment', 'click'); + await trigger(document.body, '#toggle', 'click'); + + expect((global as any).logs).toEqual([ + 'Child of "A" (0)', // init + 'Child of "A" (1)', // increment + 'Child of "B" (1)', // toggle + 'Child of "B" (2)', // increment + 'Child of "A" (2)', // toggle + ]); + }); }); describe('queue', () => { it('should execute dependant tasks', async () => { @@ -398,44 +463,44 @@ describe.each([ (globalThis as any).log = undefined; }); it('should handle promises and tasks', async () => { - const log: string[] = []; + (global as any).log = [] as string[]; const MyComp = component$(() => { - log.push('render'); const promise = useSignal>(); + (global as any).log.push('render'); // Tasks should run one after the other, awaiting returned promises. // Here we "sideload" a promise via the signal useTask$(() => { promise.value = Promise.resolve(0) .then(() => { - log.push('inside.1'); + (global as any).log.push('inside.1'); return delay(10); }) .then(() => { - log.push('1b'); + (global as any).log.push('1b'); return 1; }); - log.push('1a'); + (global as any).log.push('1a'); }); useTask$(async () => { - log.push('2a'); + (global as any).log.push('2a'); await delay(10); - log.push('2b'); + (global as any).log.push('2b'); }); useTask$(() => { promise.value = promise.value!.then(() => { - log.push('3b'); + (global as any).log.push('3b'); return 3; }); - log.push('3a'); + (global as any).log.push('3a'); }); return

    Should have a number: "{promise.value}"

    ; }); const { vNode } = await render(, { debug }); - expect(log).toEqual([ + expect((global as any).log).toEqual([ // 1st render 'render', // task 1 returns sync and sideloads promise @@ -470,8 +535,7 @@ describe.each([ }); describe('regression', () => { - // TODO(optimizer-test): problem still exists with the optimizer! - it.skip('#5782', async () => { + it('#5782', async () => { const Child = component$(({ sig }: { sig: SignalType> }) => { const counter = useSignal(0); useTask$(({ track }) => { diff --git a/packages/qwik/src/core/v2/tests/use-visible-task.spec.tsx b/packages/qwik/src/core/v2/tests/use-visible-task.spec.tsx index 89930de30f2..df1538de2fc 100644 --- a/packages/qwik/src/core/v2/tests/use-visible-task.spec.tsx +++ b/packages/qwik/src/core/v2/tests/use-visible-task.spec.tsx @@ -294,7 +294,7 @@ describe.each([ const double = useSignal(0); useVisibleTask$(({ track }) => { - double.value = 2 * track(count); + double.value = 2 * track(() => count.value); }); return ( -
    {resourceState}
    +
    {resource.loading ? "pending" : "resolved"}
    ); @@ -96,8 +85,6 @@ export const Results = component$( white-space: pre; }`); const logs = useContext(LOGS); - logs.content += "[RENDER] \n\n\n"; - const logscontent = logs.content + ""; const state = useStore({ count: 0, @@ -124,7 +111,7 @@ export const Results = component$( }} /> -
    {logscontent}
    +
    {logs.content + ""}
    ); }, diff --git a/starters/apps/e2e/src/components/signals/signals.tsx b/starters/apps/e2e/src/components/signals/signals.tsx index f082da3f0b9..95fd14f0dba 100644 --- a/starters/apps/e2e/src/components/signals/signals.tsx +++ b/starters/apps/e2e/src/components/signals/signals.tsx @@ -2,6 +2,8 @@ import { component$, type Signal, useSignal, + createSignal, + useConstant, useStore, useVisibleTask$, useTask$, @@ -11,6 +13,7 @@ import { type QwikIntrinsicElements, Resource, useComputed$, + // createComputed$, } from "@builder.io/qwik"; import { delay } from "../resource/resource"; import { @@ -134,6 +137,7 @@ export const SignalsChildren = component$(() => { + ); }); @@ -1238,3 +1242,38 @@ export const Issue4868Card = component$((props: { src: string }) => { ); }); + +export const ManySignals = component$(() => { + const signals = useConstant(() => { + const arr: (Signal | string)[] = []; + for (let i = 0; i < 10; i++) { + arr.push(createSignal(0)); + arr.push(", "); + } + return arr; + }); + // const doubles = useConstant(() => + // signals.map((s: Signal | string) => + // typeof s === "string" ? s : createComputed$(() => s.value * 2), + // ), + // ); + + return ( + <> + +
    {signals}
    + {/*
    {doubles}
    */} + + ); +}); diff --git a/starters/apps/e2e/src/components/streaming/streaming.tsx b/starters/apps/e2e/src/components/streaming/streaming.tsx index fd714b7ee0e..7282cd8c49c 100644 --- a/starters/apps/e2e/src/components/streaming/streaming.tsx +++ b/starters/apps/e2e/src/components/streaming/streaming.tsx @@ -61,15 +61,15 @@ export const Streaming = component$(() => { - - + + - + + - ); diff --git a/starters/apps/library/package.json b/starters/apps/library/package.json index 1fec5dc0978..0c388d2a471 100644 --- a/starters/apps/library/package.json +++ b/starters/apps/library/package.json @@ -43,7 +43,6 @@ "np": "^8.0.4", "prettier": "latest", "typescript": "latest", - "undici": "latest", "vite": "^4.5.2", "vite-tsconfig-paths": "^4.2.1" }, diff --git a/starters/dev-server.ts b/starters/dev-server.ts index 379da438f88..b94a64b94eb 100644 --- a/starters/dev-server.ts +++ b/starters/dev-server.ts @@ -332,8 +332,6 @@ function favicon(_: Request, res: Response) { } async function main() { - await patchGlobalFetch(); - const partytownPath = resolve( startersDir, "..", @@ -371,23 +369,3 @@ async function main() { } main(); - -async function patchGlobalFetch() { - if ( - typeof global !== "undefined" && - typeof globalThis.fetch !== "function" && - typeof process !== "undefined" && - process.versions.node - ) { - if (!globalThis.fetch) { - const { fetch, Headers, Request, Response, FormData } = await import( - "undici" - ); - globalThis.fetch = fetch as any; - globalThis.Headers = Headers as any; - globalThis.Request = Request as any; - globalThis.Response = Response as any; - globalThis.FormData = FormData as any; - } - } -} diff --git a/starters/e2e/e2e.lexical-scope.e2e.ts b/starters/e2e/e2e.lexical-scope.e2e.ts index c1923777a9c..4b26591d465 100644 --- a/starters/e2e/e2e.lexical-scope.e2e.ts +++ b/starters/e2e/e2e.lexical-scope.e2e.ts @@ -13,7 +13,7 @@ test.describe("lexical-scope", () => { test("should rerender without changes", async ({ page }) => { const SNAPSHOT = - '

    1

    "</script>"

    {"a":{"thing":12},"b":"hola","c":123,"d":false,"e":true,"f":null,"h":[1,"string",false,{"hola":1},["hello"]],"promise":{}}

    undefined

    null

    [1,2,"hola",null,{}]

    true

    false

    ()=>console.error()

    mutable message

    {"signal":{"value":0},"signalValue":0,"store":{"count":0,"signal":{"value":0}},"storeCount":0,"storeSignal":{"value":0}}

    from a promise

    message, message2, signal, signalValue, store, storeCount, storeSignal

    '; + '

    1

    "</script>"

    {"a":{"thing":12},"b":"hola","c":123,"d":false,"e":true,"f":null,"h":[1,"string",false,{"hola":1},["hello"]],"promise":{}}

    undefined

    null

    [1,2,"hola",null,{}]

    true

    false

    ()=>console.error()

    mutable message

    {"signal":{"value":0},"signalValue":0,"store":{"count":0,"signal":{"value":0}},"storeCount":0,"storeSignal":{"value":0}}

    from a promise

    message, message2, signal, signalValue, store, storeCount, storeSignal

    '; const RESULT = '[1,"",{"a":{"thing":12},"b":"hola","c":123,"d":false,"e":true,"f":null,"h":[1,"string",false,{"hola":1},["hello"]],"promise":{}},"undefined","null",[1,2,"hola",null,{}],true,false,null,"mutable message",null,{"value":0},0,{"count":0,"signal":{"value":0}},0,{"value":0},"from a promise","http://qwik.builder.com/docs?query=true","2022-07-26T17:40:30.255Z","hola()\\\\/ gi",12,"failed message",["\\b: backspace","\\f: form feed","\\n: line feed","\\r: carriage return","\\t: horizontal tab","\\u000b: vertical tab","\\u0000: null character","\': single quote","\\\\: backslash"],"Infinity","-Infinity","NaN","88","qwik",["1","2"],"200000000000000000","[\\"hola\\",12,{\\"a\\":\\"2022-07-26T17:40:30.255Z\\"}]","[[{},{}],[\\"mapkey\\",\\"http://qwik.builder.com/docs?query=true\\"]]"]'; diff --git a/starters/e2e/e2e.resource.e2e.ts b/starters/e2e/e2e.resource.e2e.ts index 42791a49dc6..39245493da0 100644 --- a/starters/e2e/e2e.resource.e2e.ts +++ b/starters/e2e/e2e.resource.e2e.ts @@ -11,13 +11,14 @@ test.describe("resource", () => { }); }); - // TODO(v2): fix this - test.skip("should load", async ({ page }) => { + test("should load", async ({ page }) => { const resource1 = page.locator(".resource1"); const logs = page.locator(".logs"); const increment = page.locator("button.increment"); let logsContent = - "[RENDER] \n[WATCH] 1 before\n[WATCH] 1 after\n[WATCH] 2 before\n[WATCH] 2 after\n[RESOURCE] 1 before\n[RENDER] \n\n\n"; + "[WATCH] 1 before\n[WATCH] 1 after\n[WATCH] 2 before\n[WATCH] 2 after\n[RESOURCE] 1 before\n"; + // TODO: server promise streaming is not supported, so for now we can't test this correctly + logsContent += "[RESOURCE] 1 after\n\n"; await expect(resource1).toHaveText("resource 1 is 80"); // await expect(resource2).toHaveText('resource 2 is 160'); await expect(logs).toHaveText(logsContent); @@ -27,22 +28,23 @@ test.describe("resource", () => { await expect(resource1).toHaveText("loading resource 1..."); logsContent += - "[RESOURCE] 1 after\n\n[WATCH] 1 before\n[WATCH] 1 after\n[WATCH] 2 before\n[WATCH] 2 after\n[RESOURCE] 1 before\n[RENDER] \n\n\n"; + "[WATCH] 1 before\n[WATCH] 1 after\n[WATCH] 2 before\n[WATCH] 2 after\n[RESOURCE] 1 before\n"; // await expect(resource2).toHaveText('loading resource 2...'); await expect(logs).toHaveText(logsContent); await expect(resource1).toHaveText("resource 1 is 88"); - logsContent += "[RESOURCE] 1 after\n[RENDER] \n\n\n"; + logsContent += "[RESOURCE] 1 after\n\n"; // await expect(resource2).toHaveText('resource 2 is 176'); await expect(logs).toHaveText(logsContent); }); - // TODO(v2): fix this - test.skip("should track subscriptions", async ({ page }) => { + test("should track subscriptions", async ({ page }) => { const resource1 = page.locator(".resource1"); const logs = page.locator(".logs"); let logsContent = - "[RENDER] \n[WATCH] 1 before\n[WATCH] 1 after\n[WATCH] 2 before\n[WATCH] 2 after\n[RESOURCE] 1 before\n[RENDER] \n\n\n"; + "[WATCH] 1 before\n[WATCH] 1 after\n[WATCH] 2 before\n[WATCH] 2 after\n[RESOURCE] 1 before\n"; + // TODO: server promise streaming is not supported, so for now we can't test this correctly + logsContent += "[RESOURCE] 1 after\n\n"; await expect(resource1).toHaveText("resource 1 is 80"); await expect(logs).toHaveText(logsContent); @@ -52,14 +54,11 @@ test.describe("resource", () => { await countBtn.click(); await expect(countBtn).toHaveText("count is 1"); - logsContent += "[RESOURCE] 1 after\n[RENDER] \n\n\n"; + // logsContent += "[RESOURCE] 1 after\n\n"; await expect(logs).toHaveText(logsContent); await countBtn.click(); await expect(countBtn).toHaveText("count is 2"); - - logsContent += "[RENDER] \n\n\n"; - await expect(logs).toHaveText(logsContent); }); }); @@ -114,8 +113,7 @@ test.describe("resource serialization", () => { await expect(button1).toHaveText("4(count is here: 2)"); }); - // TODO(v2): fix this - test.skip("race condition", async ({ page }) => { + test("race condition", async ({ page }) => { const btn = page.locator("#resource-race-btn"); const result = page.locator("#resource-race-result"); diff --git a/starters/e2e/e2e.signals.e2e.ts b/starters/e2e/e2e.signals.e2e.ts index 42c0c5a5574..2edaed25f12 100644 --- a/starters/e2e/e2e.signals.e2e.ts +++ b/starters/e2e/e2e.signals.e2e.ts @@ -557,6 +557,18 @@ test.describe("signals", () => { `Card useComputed$: https://placehold.co/400x400?text=1&useComputed$`, ); }); + + test.skip("createSignal/createComputed$", async ({ page }) => { + const button = page.locator("#many-signals-button"); + const result = page.locator("#many-signals-result"); + // TODO createComputed$ + // const doubles = page.locator("#many-doubles-result"); + await expect(result).toHaveText("0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "); + // await expect(doubles).toHaveText("0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "); + await button.click(); + await expect(result).toHaveText("1, 1, 1, 1, 1, 1, 1, 1, 1, 1, "); + // await expect(doubles).toHaveText("2, 2, 2, 2, 2, 2, 2, 2, 2, 2, "); + }); } tests(); diff --git a/starters/e2e/e2e.slot.e2e.ts b/starters/e2e/e2e.slot.e2e.ts index 91859ef10a2..0e06dcb612f 100644 --- a/starters/e2e/e2e.slot.e2e.ts +++ b/starters/e2e/e2e.slot.e2e.ts @@ -90,8 +90,7 @@ test.describe("slot", () => { }); }); - // TODO(v2): fix this - test.skip("should toggle content", async ({ page }) => { + test("should toggle content", async ({ page }) => { const content1 = page.locator("#btn1"); const content2 = page.locator("#btn2"); const content3 = page.locator("#btn3"); @@ -115,8 +114,7 @@ test.describe("slot", () => { }); }); - // TODO(v2): fix this - test.skip("should toggle content and buttons", async ({ page }) => { + test("should toggle content and buttons", async ({ page }) => { const content1 = page.locator("#btn1"); const content2 = page.locator("#btn2"); const content3 = page.locator("#btn3"); @@ -214,8 +212,7 @@ test.describe("slot", () => { await expect(modalContent).not.toBeHidden(); }); - // TODO(v2): fix this - test.skip("issue 2688", async ({ page }) => { + test("issue 2688", async ({ page }) => { const result = page.locator("#issue-2688-result"); const button = page.locator("#issue-2688-button"); const count = page.locator("#btn-count"); diff --git a/starters/e2e/e2e.streaming.e2e.ts b/starters/e2e/e2e.streaming.e2e.ts index eae4be82c69..8c1b2ead8d8 100644 --- a/starters/e2e/e2e.streaming.e2e.ts +++ b/starters/e2e/e2e.streaming.e2e.ts @@ -42,8 +42,7 @@ test.describe("streaming", () => { await expect(cmps).toHaveCount(5); }); - // TODO(v2): fix this - test.skip("should render in client correctly", async ({ page }) => { + test("should render in client correctly", async ({ page }) => { const ul = page.locator("ul > li"); const ol = page.locator("ol > li"); const cmps = page.locator(".cmp"); diff --git a/starters/e2e/e2e.toggle.e2e.ts b/starters/e2e/e2e.toggle.e2e.ts index 34004453ddf..492cd726029 100644 --- a/starters/e2e/e2e.toggle.e2e.ts +++ b/starters/e2e/e2e.toggle.e2e.ts @@ -11,8 +11,7 @@ test.describe("toggle", () => { }); }); - // TODO(v2): fix this - test.skip("should load", async ({ page }) => { + test("should load", async ({ page }) => { const title = page.locator("h1"); const mount = page.locator("#mount"); const root = page.locator("#root"); @@ -46,7 +45,7 @@ test.describe("toggle", () => { // ToggleB await btnToggle.click(); - logsStr += "Child(1)ToggleB()"; + logsStr += "ToggleB()Child(1)"; await expect(title).toHaveText("ToggleA"); await expect(mount).toHaveText("mounted in client"); @@ -65,7 +64,7 @@ test.describe("toggle", () => { // ToggleA + increment await btnToggle.click(); await btnIncrement.click(); - logsStr += "Child(2)ToggleA()Log(3)Child(3)"; + logsStr += "ToggleA()Child(2)Log(3)Child(3)"; await expect(title).toHaveText("ToggleB"); await expect(mount).toHaveText("mounted in client"); diff --git a/starters/e2e/qwikcity/locale.e2e.ts b/starters/e2e/qwikcity/locale.e2e.ts index 2bbbd5351e1..8bd551c56e0 100644 --- a/starters/e2e/qwikcity/locale.e2e.ts +++ b/starters/e2e/qwikcity/locale.e2e.ts @@ -1,12 +1,12 @@ import { expect, test } from "@playwright/test"; +import { QContainerSelector } from "../../../packages/qwik/src/core/util/markers"; test.describe("Qwik City locale API", () => { - // TODO(v2): fix this - test.skip("pass locale to Qwik", async ({ page }) => { + test("pass locale to Qwik", async ({ page }) => { await page.goto("/qwikcity-test/locale"); const locale = page.locator(".locale"); - const qContainer = page.locator("[q\\:container]"); await expect(locale).toHaveText("test-locale"); + const qContainer = page.locator(QContainerSelector); await expect(qContainer).toHaveAttribute("q:locale", "test-locale"); }); }); diff --git a/starters/e2e/qwikcity/menu.e2e.ts b/starters/e2e/qwikcity/menu.e2e.ts index 823118e1e34..915d39e3c15 100644 --- a/starters/e2e/qwikcity/menu.e2e.ts +++ b/starters/e2e/qwikcity/menu.e2e.ts @@ -6,8 +6,7 @@ test.describe("Qwik City Menu", () => { test.use({ javaScriptEnabled: false }); tests(); }); - // TODO(v2): fix this - test.describe.skip("spa", () => { + test.describe("spa", () => { test.use({ javaScriptEnabled: true }); tests(); }); diff --git a/starters/e2e/qwikcity/page.e2e.ts b/starters/e2e/qwikcity/page.e2e.ts index 2710a2379c5..e89f400f23c 100644 --- a/starters/e2e/qwikcity/page.e2e.ts +++ b/starters/e2e/qwikcity/page.e2e.ts @@ -7,7 +7,7 @@ test.describe("Qwik City Page", () => { tests(); }); - // TODO(v2): fix this + // TODO(v2): this should be fixed after signals v2 is implemented test.describe.skip("spa", () => { test.use({ javaScriptEnabled: true }); tests(); diff --git a/starters/features/auth/package.json b/starters/features/auth/package.json index 3914ee5c797..259c797d415 100644 --- a/starters/features/auth/package.json +++ b/starters/features/auth/package.json @@ -17,7 +17,7 @@ } }, "devDependencies": { - "@auth/core": "^0.30.0", - "@builder.io/qwik-auth": "0.1.1" + "@auth/core": "0.31.0", + "@builder.io/qwik-auth": "0.2.2" } } diff --git a/tsm.cjs b/tsm.cjs index f4eedc6d7cb..b5aff3a5154 100644 --- a/tsm.cjs +++ b/tsm.cjs @@ -16,21 +16,6 @@ globalThis.qDev = true; globalThis.qInspector = false; import * as qwikJsx from "${corePath}";`; -if ( - typeof global !== 'undefined' && - typeof globalThis.fetch !== 'function' && - typeof process !== 'undefined' && - process.versions.node -) { - if (!globalThis.fetch) { - const { fetch, Headers, Request, Response, FormData } = require('undici'); - globalThis.fetch = fetch; - globalThis.Headers = Headers; - globalThis.Request = Request; - globalThis.Response = Response; - globalThis.FormData = FormData; - } -} module.exports = { common: { minifyWhitespace: true,