From 646eaf57fa43405f0f961d041ba9d152115d5f8c Mon Sep 17 00:00:00 2001 From: wuls Date: Fri, 20 Dec 2024 14:42:46 +0800 Subject: [PATCH 1/9] bug: fix --- .../qwik/src/core/shared/jsx/types/jsx-generated.ts | 2 +- packages/qwik/src/server/ssr-container.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts b/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts index a21dd50bb50..ba0b9ec8231 100644 --- a/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts +++ b/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts @@ -596,7 +596,7 @@ type SpecialAttrs = { * For type: HTMLInputTypeAttribute, excluding 'button' | 'reset' | 'submit' | 'checkbox' | * 'radio' */ - 'bind:value'?: Signal; + 'bind:value'?: Signal; enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined; height?: Size | undefined; max?: number | string | undefined; diff --git a/packages/qwik/src/server/ssr-container.ts b/packages/qwik/src/server/ssr-container.ts index 8a01156f10a..d542fd228f1 100644 --- a/packages/qwik/src/server/ssr-container.ts +++ b/packages/qwik/src/server/ssr-container.ts @@ -1165,14 +1165,20 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { key = QContainerAttr; value = QContainerValue.TEXT; } - const serializedValue = serializeAttribute(key, value, styleScopedId); if (serializedValue != null && serializedValue !== false) { this.write(' '); this.write(key); - if (serializedValue !== true) { - this.write('="'); + if (typeof serializedValue === 'number'){ + this.write('='); + const strValue = escapeHTML(serializedValue); + this.write(strValue); + this.write(''); + }else if (serializedValue !== true) { + this.write('="'); + + const strValue = escapeHTML(String(serializedValue)); this.write(strValue); From 327c75e5277d7ac1f5abef4864d1f9645e670517 Mon Sep 17 00:00:00 2001 From: wuls Date: Fri, 20 Dec 2024 17:00:04 +0800 Subject: [PATCH 2/9] bug: fix --- packages/qwik/src/core/shared/utils/styles.ts | 2 +- packages/qwik/src/server/ssr-container.ts | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/qwik/src/core/shared/utils/styles.ts b/packages/qwik/src/core/shared/utils/styles.ts index 8b21c34f892..1c682ca9647 100644 --- a/packages/qwik/src/core/shared/utils/styles.ts +++ b/packages/qwik/src/core/shared/utils/styles.ts @@ -70,7 +70,7 @@ export const stringifyStyle = (obj: any): string => { }; export const serializeBooleanOrNumberAttribute = (value: any) => { - return value != null ? String(value) : null; + return value != null ? (typeof value === 'number' ? value : String(value)) : null; }; export function serializeAttribute( diff --git a/packages/qwik/src/server/ssr-container.ts b/packages/qwik/src/server/ssr-container.ts index 0802a78860a..a777ef746e8 100644 --- a/packages/qwik/src/server/ssr-container.ts +++ b/packages/qwik/src/server/ssr-container.ts @@ -1186,18 +1186,15 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { if (serializedValue != null && serializedValue !== false) { this.write(' '); this.write(key); - if (typeof serializedValue === 'number'){ + if (typeof serializedValue === 'number') { this.write('='); const strValue = escapeHTML(serializedValue); this.write(strValue); this.write(''); - }else if (serializedValue !== true) { - this.write('="'); - - + } else if (serializedValue !== true) { + this.write('="'); const strValue = escapeHTML(String(serializedValue)); this.write(strValue); - this.write('"'); } } From db8e644149d145669f244f9f98b3a474b304da10 Mon Sep 17 00:00:00 2001 From: wuls Date: Mon, 23 Dec 2024 10:04:12 +0800 Subject: [PATCH 3/9] merget --- .changeset/chilled-spoons-wonder.md | 5 + .changeset/config.json | 2 +- .changeset/friendly-gorillas-walk.md | 5 + .changeset/heavy-seas-carry.md | 5 + .changeset/mean-dingos-hug.md | 5 + .changeset/nervous-terms-explode.md | 5 + .changeset/pre.json | 8 + .changeset/tiny-berries-bow.md | 5 + .changeset/twenty-goats-flow.md | 5 + .github/workflows/ci.yml | 22 +- packages/create-qwik/CHANGELOG.md | 4 + packages/create-qwik/package.json | 2 +- packages/docs/package.json | 2 +- .../components/docsearch/doc-search-modal.tsx | 2 +- .../docs/src/components/qwik-gpt/index.tsx | 2 +- packages/docs/src/repl/monaco.tsx | 3 +- packages/docs/src/repl/repl-output-update.ts | 16 +- packages/docs/src/repl/repl.tsx | 2 +- packages/docs/src/routes/api/index.tsx | 3 +- packages/docs/src/routes/api/qwik/api.json | 2 +- .../cookbook/drag&drop/advanced/index.tsx | 193 ++++++++++ .../demo/cookbook/drag&drop/basic/index.tsx | 119 ++++++ .../src/routes/demo/tasks/track-fn/index.tsx | 3 +- .../demo/tasks/track-server-guard/index.tsx | 3 +- .../src/routes/demo/tasks/track/index.tsx | 3 +- .../docs/(qwik)/components/styles/index.mdx | 25 ++ .../docs/(qwik)/components/tasks/index.mdx | 11 +- .../content-security-policy/index.mdx | 4 +- .../guides/qwik-nutshell/index.mdx | 25 +- .../routes/docs/cookbook/drag&drop/index.mdx | 358 ++++++++++++++++++ .../docs/src/routes/docs/cookbook/index.mdx | 1 + packages/docs/src/routes/docs/menu.md | 3 +- .../src/routes/examples/[...id]/index!.tsx | 12 +- .../apps/partial/hackernews-index/app.tsx | 3 +- .../docs/src/routes/playground/index!.tsx | 11 +- packages/docs/vite.repl-apps.ts | 4 + packages/eslint-plugin-qwik/CHANGELOG.md | 4 + packages/eslint-plugin-qwik/package.json | 2 +- .../valid-inside-component.tsx | 3 +- packages/insights/src/db/logging.ts | 2 +- packages/qwik-react/CHANGELOG.md | 14 + packages/qwik-react/package.json | 2 +- packages/qwik-react/src/react/qwikify.tsx | 13 +- .../qwik-react/src/react/server-render.tsx | 3 +- packages/qwik-react/src/react/slot.ts | 3 +- packages/qwik-router/CHANGELOG.md | 4 + packages/qwik-router/package.json | 5 +- .../src/runtime/src/client-navigate.ts | 2 +- .../src/runtime/src/link-component.tsx | 11 +- .../src/runtime/src/qwik-router-component.tsx | 4 +- .../src/runtime/src/server-functions.ts | 2 +- .../qwik-router/src/runtime/src/spa-init.ts | 3 +- packages/qwik/CHANGELOG.md | 24 ++ packages/qwik/package.json | 6 +- packages/qwik/public.d.ts | 3 + .../src/cli/migrate-v2/replace-package.ts | 10 +- .../qwik/src/cli/migrate-v2/run-migration.ts | 8 + .../cli/migrate-v2/update-configurations.ts | 20 + .../src/cli/migrate-v2/update-dependencies.ts | 1 + packages/qwik/src/core/api.md | 29 +- .../qwik/src/core/client/dom-container.ts | 16 +- packages/qwik/src/core/client/dom-render.ts | 5 + packages/qwik/src/core/client/vnode-diff.ts | 130 ++++--- packages/qwik/src/core/client/vnode.ts | 185 ++++++--- packages/qwik/src/core/index.ts | 4 +- .../src/core/shared/component-execution.ts | 2 +- packages/qwik/src/core/shared/error/error.ts | 159 +++++--- .../core/shared/jsx/types/jsx-generated.ts | 2 +- .../qwik/src/core/shared/platform/platform.ts | 7 +- .../prefetch-service-worker/prefetch.ts | 1 + .../qwik/src/core/shared/qrl/qrl-class.ts | 4 +- packages/qwik/src/core/shared/qrl/qrl.ts | 6 +- packages/qwik/src/core/shared/scheduler.ts | 93 +++-- .../qwik/src/core/shared/scheduler.unit.tsx | 1 - .../qwik/src/core/shared/shared-container.ts | 16 +- .../src/core/shared/shared-serialization.ts | 85 +++-- .../core/shared/shared-serialization.unit.ts | 27 +- packages/qwik/src/core/shared/types.ts | 4 - packages/qwik/src/core/shared/utils/log.ts | 2 +- .../qwik/src/core/shared/utils/markers.ts | 1 + .../qwik/src/core/shared/utils/promises.ts | 2 +- .../src/core/shared/utils/serialize-utils.ts | 8 +- packages/qwik/src/core/shared/utils/styles.ts | 6 +- .../qwik/src/core/shared/utils/styles.unit.ts | 2 +- .../qwik/src/core/shared/vnode-data-types.ts | 14 +- .../qwik/src/core/signal/signal-subscriber.ts | 37 +- packages/qwik/src/core/signal/signal.ts | 86 ++--- packages/qwik/src/core/ssr/ssr-render-jsx.ts | 12 +- packages/qwik/src/core/tests/TEST.md | 45 --- .../qwik/src/core/tests/attributes.spec.tsx | 65 ++++ .../qwik/src/core/tests/component.spec.tsx | 20 +- .../qwik/src/core/tests/projection.spec.tsx | 211 ++++++++++- .../qwik/src/core/tests/render-api.spec.tsx | 2 +- .../qwik/src/core/tests/use-computed.spec.tsx | 50 ++- .../qwik/src/core/tests/use-signal.spec.tsx | 119 ++++-- .../qwik/src/core/tests/use-store.spec.tsx | 65 ++++ packages/qwik/src/core/use/use-context.ts | 6 +- packages/qwik/src/core/use/use-core.ts | 33 +- .../src/optimizer/core/src/const_replace.rs | 13 + ...qwik_core__test__example_build_server.snap | 18 +- .../qwik_core__test__example_qwik_react.snap | 14 +- ...core__test__example_qwik_react_inline.snap | 6 +- ...core__test__example_strip_server_code.snap | 26 +- packages/qwik/src/optimizer/core/src/test.rs | 18 +- packages/qwik/src/server/qwik-copy.ts | 2 + packages/qwik/src/server/qwik-types.ts | 2 +- packages/qwik/src/server/ssr-container.ts | 97 +++-- packages/qwik/src/server/types.ts | 20 + packages/qwik/src/server/vnode-data.ts | 88 ++--- .../qwik/src/testing/vdom-diff.unit-util.ts | 24 +- scripts/api-docs.ts | 12 +- scripts/api.ts | 4 - scripts/submodule-server.ts | 1 + .../src/components/build-variables/build.tsx | 9 +- .../apps/e2e/src/components/mount/mount.tsx | 3 +- .../apps/e2e/src/components/render/render.tsx | 2 +- .../e2e/src/components/signals/signals.tsx | 8 +- .../apps/e2e/src/components/toggle/toggle.tsx | 3 +- .../apps/e2e/src/components/watch/watch.tsx | 2 +- starters/apps/empty/src/root.tsx | 3 +- starters/apps/playground/src/root.tsx | 3 +- .../issue-loader-serialization/index.tsx | 4 +- 122 files changed, 2237 insertions(+), 739 deletions(-) create mode 100644 .changeset/chilled-spoons-wonder.md create mode 100644 .changeset/friendly-gorillas-walk.md create mode 100644 .changeset/heavy-seas-carry.md create mode 100644 .changeset/mean-dingos-hug.md create mode 100644 .changeset/nervous-terms-explode.md create mode 100644 .changeset/tiny-berries-bow.md create mode 100644 .changeset/twenty-goats-flow.md create mode 100644 packages/docs/src/routes/demo/cookbook/drag&drop/advanced/index.tsx create mode 100644 packages/docs/src/routes/demo/cookbook/drag&drop/basic/index.tsx create mode 100644 packages/docs/src/routes/docs/cookbook/drag&drop/index.mdx create mode 100644 packages/qwik/src/cli/migrate-v2/update-configurations.ts delete mode 100644 packages/qwik/src/core/tests/TEST.md diff --git a/.changeset/chilled-spoons-wonder.md b/.changeset/chilled-spoons-wonder.md new file mode 100644 index 00000000000..262ae2d7438 --- /dev/null +++ b/.changeset/chilled-spoons-wonder.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +fix: prevent multiple store deserialization diff --git a/.changeset/config.json b/.changeset/config.json index 76fa6e5b408..9522e2b7f2f 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,7 +7,7 @@ ], "linked": [], "access": "public", - "baseBranch": "build/v2", + "baseBranch": "origin/build/v2", "updateInternalDependencies": "minor", "ignore": ["qwik-docs", "insights", "qwik-cli-e2e"], "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { diff --git a/.changeset/friendly-gorillas-walk.md b/.changeset/friendly-gorillas-walk.md new file mode 100644 index 00000000000..803223e3e70 --- /dev/null +++ b/.changeset/friendly-gorillas-walk.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +FIX: types error when migrating to V2 with `moduleResulution: "node"` diff --git a/.changeset/heavy-seas-carry.md b/.changeset/heavy-seas-carry.md new file mode 100644 index 00000000000..d5d1582094b --- /dev/null +++ b/.changeset/heavy-seas-carry.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +fix: encode the `q:subs` property diff --git a/.changeset/mean-dingos-hug.md b/.changeset/mean-dingos-hug.md new file mode 100644 index 00000000000..705ba076cb2 --- /dev/null +++ b/.changeset/mean-dingos-hug.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +feat: move signal invalidation to the scheduler diff --git a/.changeset/nervous-terms-explode.md b/.changeset/nervous-terms-explode.md new file mode 100644 index 00000000000..3ac190db6c4 --- /dev/null +++ b/.changeset/nervous-terms-explode.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +feat: better node attributes serialization diff --git a/.changeset/pre.json b/.changeset/pre.json index 881bf9912bc..54076c0ca2d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -17,17 +17,25 @@ "changesets": [ "brave-files-grin", "calm-cycles-know", + "chilled-spoons-wonder", + "clever-flowers-drum", "fast-baboons-itch", "five-kangaroos-matter", "fresh-rocks-exercise", + "friendly-gorillas-walk", + "heavy-seas-carry", "hip-hornets-cheer", + "mean-dingos-hug", + "nervous-terms-explode", "nine-otters-repeat", "proud-pillows-try", "rich-wasps-tease", "rotten-weeks-tickle", "sour-zebras-tell", "sweet-socks-whisper", + "tiny-berries-bow", "tricky-meals-heal", + "twenty-goats-flow", "wild-cooks-pay" ] } diff --git a/.changeset/tiny-berries-bow.md b/.changeset/tiny-berries-bow.md new file mode 100644 index 00000000000..46a4ba2ad84 --- /dev/null +++ b/.changeset/tiny-berries-bow.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +fix: serialize virtual props for DOM elements diff --git a/.changeset/twenty-goats-flow.md b/.changeset/twenty-goats-flow.md new file mode 100644 index 00000000000..64cbd9ca066 --- /dev/null +++ b/.changeset/twenty-goats-flow.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +fix: replacing projection content with null or undefined diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2808468031e..9c29315c713 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -859,11 +859,25 @@ jobs: # Delete this after V2 is released - name: Tag with latest - if: steps.changesets.outcome == 'success' + if: steps.changesets.outputs.published == 'true' run: | - npm dist-tag add @qwik.dev/core@${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }} latest - npm dist-tag add @qwik.dev/router@${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }} latest - npm dist-tag add @qwik.dev/react@${{ fromJson(steps.changesets.outputs.publishedPackages)[0].version }} latest + if [ -f "$HOME/.npmrc" ]; then + echo "Found existing user .npmrc file" + if grep -qi "^[[:space:]]*//registry.npmjs.org/:[_-]authToken=" "$HOME/.npmrc"; then + echo "Found existing auth token for the npm registry in the user .npmrc file" + else + echo "Didn't find existing auth token for the npm registry in the user .npmrc file, creating one" + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> "$HOME/.npmrc" + fi + else + echo "No user .npmrc file found, creating one" + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$HOME/.npmrc" + fi + npm dist-tag add @qwik.dev/core@${{ fromJSON(steps.changesets.outputs.publishedPackages)[0].version }} latest + npm dist-tag add @qwik.dev/router@${{ fromJSON(steps.changesets.outputs.publishedPackages)[0].version }} latest + npm dist-tag add @qwik.dev/react@${{ fromJSON(steps.changesets.outputs.publishedPackages)[0].version }} latest + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Fixup package.json files run: pnpm run release.fixup-package-json diff --git a/packages/create-qwik/CHANGELOG.md b/packages/create-qwik/CHANGELOG.md index bba8845f93a..703b297c47c 100644 --- a/packages/create-qwik/CHANGELOG.md +++ b/packages/create-qwik/CHANGELOG.md @@ -1,5 +1,9 @@ # create-qwik +## 2.0.0-alpha.4 + +## 2.0.0-alpha.3 + ## 2.0.0-alpha.2 ## 2.0.0-alpha.1 diff --git a/packages/create-qwik/package.json b/packages/create-qwik/package.json index e89f911cb8a..d432760382f 100644 --- a/packages/create-qwik/package.json +++ b/packages/create-qwik/package.json @@ -1,7 +1,7 @@ { "name": "create-qwik", "description": "Interactive CLI for create Qwik projects and adding features.", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "author": "Qwik Team", "bin": "./create-qwik.cjs", "bugs": "https://github.com/QwikDev/qwik/issues", diff --git a/packages/docs/package.json b/packages/docs/package.json index d3e0ae0acbb..8f46d17afd7 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -68,7 +68,7 @@ "license": "MIT", "private": true, "scripts": { - "build": "qwik build", + "build": "pnpm build.repl-sw && qwik build", "build.client": "vite build", "build.preview": "NODE_OPTIONS=--max-old-space-size=8192 vite build --ssr src/entry.preview.tsx", "build.repl-sw": "vite --config vite.config-repl-sw build", diff --git a/packages/docs/src/components/docsearch/doc-search-modal.tsx b/packages/docs/src/components/docsearch/doc-search-modal.tsx index 6d317f974a7..5126ad46be2 100644 --- a/packages/docs/src/components/docsearch/doc-search-modal.tsx +++ b/packages/docs/src/components/docsearch/doc-search-modal.tsx @@ -12,7 +12,7 @@ import type { DocSearchHit } from './types'; import { identity } from './utils'; import { clearStalled, setStalled } from './utils/stalledControl'; import { AIButton } from './result'; -import { isBrowser } from '@qwik.dev/core/build'; +import { isBrowser } from '@qwik.dev/core'; export type ModalTranslations = Partial<{ searchBox: SearchBoxTranslations; diff --git a/packages/docs/src/components/qwik-gpt/index.tsx b/packages/docs/src/components/qwik-gpt/index.tsx index f57c10be7b1..3a195f63e57 100644 --- a/packages/docs/src/components/qwik-gpt/index.tsx +++ b/packages/docs/src/components/qwik-gpt/index.tsx @@ -1,7 +1,7 @@ import { component$, useComputed$, useSignal } from '@qwik.dev/core'; // import { qwikGPT, rateResponse } from './search'; import { CodeBlock } from '../code-block/code-block'; -// import { isBrowser } from '@qwik.dev/core/build'; +// import { isBrowser } from '@builder.io/qwik'; import snarkdown from 'snarkdown'; const snarkdownEnhanced = (md: string) => { diff --git a/packages/docs/src/repl/monaco.tsx b/packages/docs/src/repl/monaco.tsx index c61614b8970..a6d82629d4c 100644 --- a/packages/docs/src/repl/monaco.tsx +++ b/packages/docs/src/repl/monaco.tsx @@ -1,5 +1,4 @@ -import { noSerialize } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { isServer, noSerialize } from '@qwik.dev/core'; import type { Diagnostic } from '@qwik.dev/core/optimizer'; import type MonacoTypes from 'monaco-editor'; import { getColorPreference } from '../components/theme-toggle/theme-toggle'; diff --git a/packages/docs/src/repl/repl-output-update.ts b/packages/docs/src/repl/repl-output-update.ts index 2f422ce537b..8b8f23044f0 100644 --- a/packages/docs/src/repl/repl-output-update.ts +++ b/packages/docs/src/repl/repl-output-update.ts @@ -11,9 +11,19 @@ const deepUpdate = (prev: any, next: any) => { } } } - for (const key in prev) { - if (!(key in next)) { - delete prev[key]; + if (Array.isArray(prev)) { + for (const key in prev) { + if (!(key in next)) { + delete prev[key]; + // deleting array elements doesn't change the length + prev.length--; + } + } + } else { + for (const key in prev) { + if (!(key in next)) { + delete prev[key]; + } } } }; diff --git a/packages/docs/src/repl/repl.tsx b/packages/docs/src/repl/repl.tsx index 7cf5152bc6c..62e2b561da7 100644 --- a/packages/docs/src/repl/repl.tsx +++ b/packages/docs/src/repl/repl.tsx @@ -2,13 +2,13 @@ import { $, component$, + isServer, noSerialize, useStore, useStyles$, useTask$, useVisibleTask$, } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; import { QWIK_PKG_NAME, QWIK_PKG_NAME_V1, bundled, getNpmCdnUrl } from './bundled'; import { ReplDetailPanel } from './repl-detail-panel'; import { ReplInputPanel } from './repl-input-panel'; diff --git a/packages/docs/src/routes/api/index.tsx b/packages/docs/src/routes/api/index.tsx index fdba8230ff2..a75842dc2fd 100644 --- a/packages/docs/src/routes/api/index.tsx +++ b/packages/docs/src/routes/api/index.tsx @@ -1,5 +1,4 @@ -import { $, component$, useOn, useSignal, useStore, useTask$ } from '@qwik.dev/core'; -import { isBrowser } from '@qwik.dev/core/build'; +import { $, component$, isBrowser, useOn, useSignal, useStore, useTask$ } from '@qwik.dev/core'; import { toSnakeCase } from '../../utils/utils'; // TODO: load the content of these files using fs instead of importing them diff --git a/packages/docs/src/routes/api/qwik/api.json b/packages/docs/src/routes/api/qwik/api.json index fe1f23a2be3..3595bd3ec82 100644 --- a/packages/docs/src/routes/api/qwik/api.json +++ b/packages/docs/src/routes/api/qwik/api.json @@ -2214,4 +2214,4 @@ "mdFile": "core.withlocale.md" } ] -} +} \ No newline at end of file diff --git a/packages/docs/src/routes/demo/cookbook/drag&drop/advanced/index.tsx b/packages/docs/src/routes/demo/cookbook/drag&drop/advanced/index.tsx new file mode 100644 index 00000000000..b0c1b02311f --- /dev/null +++ b/packages/docs/src/routes/demo/cookbook/drag&drop/advanced/index.tsx @@ -0,0 +1,193 @@ +import { component$, sync$, useSignal, $ } from '@builder.io/qwik'; + +type Item = { + id: number; + content: string; +}; + +export default component$(() => { + const items1 = useSignal([ + { id: 1, content: '📱 Phone' }, + { id: 2, content: '💻 Laptop' }, + { id: 3, content: '🎧 Headphones' }, + ]); + + const items2 = useSignal([ + { id: 4, content: '⌚️ Watch' }, + { id: 5, content: '🖱 Mouse' }, + { id: 6, content: '⌨️ Keyboard' }, + ]); + + return ( +
+
{ + currentTarget.setAttribute('data-over', 'true'); + })} + onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => { + currentTarget.removeAttribute('data-over'); + })} + onDrop$={[ + sync$((e: DragEvent, currentTarget: HTMLDivElement) => { + const id = e.dataTransfer?.getData('text/plain'); + currentTarget.dataset.droppedId = id; + currentTarget.removeAttribute('data-over'); + }), + $((e, currentTarget) => { + const draggedElementId = currentTarget.dataset.droppedId; + const isDropZone = currentTarget.hasAttribute('data-dropzone'); + + if (draggedElementId) { + const itemId = parseInt(draggedElementId); + const item = items2.value.find((i) => i.id === itemId); + + if (item && isDropZone) { + items2.value = items2.value.filter((i) => i.id !== itemId); + items1.value = [...items1.value, item]; + } else { + const newItems = [...items1.value]; + const targetId = parseInt( + (e.target as HTMLDivElement).dataset.id || '0' + ); + if (targetId === 0) return; + + const targetIndex = items1.value.findIndex( + (i) => i.id === targetId + ); + const draggedIndex = items1.value.findIndex( + (i) => i.id === itemId + ); + + if (draggedIndex !== -1) { + // Sorting in the same container + swapElements(newItems, draggedIndex, targetIndex); + items1.value = newItems; + } else { + // Sorting between containers + if (!item) return; + items2.value = items2.value.filter((i) => i.id !== itemId); + insertElement(newItems, targetIndex, item); + items1.value = newItems; + } + } + } + }), + ]} + > +

Container 1

+ {items1.value.map((item) => ( +
{ + const itemId = currentTarget.getAttribute('data-id'); + if (e.dataTransfer && itemId) { + e.dataTransfer.setData('text/plain', itemId); + } + } + )} + > + {item.content} +
+ ))} +
+ +
{ + currentTarget.setAttribute('data-over', 'true'); + }} + onDragLeave$={[ + sync$((_: DragEvent, currentTarget: HTMLDivElement) => { + currentTarget.removeAttribute('data-over'); + }), + ]} + onDrop$={[ + sync$((e: DragEvent, currentTarget: HTMLDivElement) => { + const id = e.dataTransfer?.getData('text/plain'); + currentTarget.dataset.droppedId = id; + currentTarget.removeAttribute('data-over'); + }), + $((e, currentTarget) => { + const draggedElementId = currentTarget.dataset.droppedId; + const isDropZone = currentTarget.hasAttribute('data-dropzone'); + + if (draggedElementId) { + const itemId = parseInt(draggedElementId); + const item = items1.value.find((i) => i.id === itemId); + + if (isDropZone && item) { + items1.value = items1.value.filter((i) => i.id !== itemId); + items2.value = [...items2.value, item]; + } else { + const targetId = parseInt( + (e.target as HTMLDivElement).dataset.id || '0' + ); + if (targetId === 0) return; + const newItems = [...items2.value]; + const draggedIndex = items2.value.findIndex( + (i) => i.id === itemId + ); + const targetIndex = items2.value.findIndex( + (i) => i.id === targetId + ); + if (draggedIndex !== -1) { + // Sorting in the same container + swapElements(newItems, targetIndex, draggedIndex); + items2.value = newItems; + } else { + // Sorting between containers + if (!item) return; + items1.value = items1.value.filter((i) => i.id !== itemId); + insertElement(newItems, targetIndex, item); + items2.value = newItems; + } + } + } + }), + ]} + > +

Container 2

+ {items2.value.map((item) => ( +
{ + const itemId = currentTarget.getAttribute('data-id'); + if (e.dataTransfer && itemId) { + e.dataTransfer.setData('text/plain', itemId); + } + } + )} + > + {item.content} +
+ ))} +
+
+ ); +}); + +function swapElements(arr: Item[], index1: number, index2: number) { + arr[index1] = arr.splice(index2, 1, arr[index1])[0]; + + return arr; +} + +function insertElement(arr: Item[], index: number, item: Item) { + arr.splice(index, 0, item); + return arr; +} diff --git a/packages/docs/src/routes/demo/cookbook/drag&drop/basic/index.tsx b/packages/docs/src/routes/demo/cookbook/drag&drop/basic/index.tsx new file mode 100644 index 00000000000..ba1a1d470a3 --- /dev/null +++ b/packages/docs/src/routes/demo/cookbook/drag&drop/basic/index.tsx @@ -0,0 +1,119 @@ +import { component$, sync$, useSignal, $ } from '@builder.io/qwik'; + +export default component$(() => { + const items1 = useSignal([ + { id: 1, content: '📱 Phone' }, + { id: 2, content: '💻 Laptop' }, + { id: 3, content: '🎧 Headphones' }, + ]); + + const items2 = useSignal([ + { id: 4, content: '⌚️ Watch' }, + { id: 5, content: '🖱 Mouse' }, + { id: 6, content: '⌨️ Keyboard' }, + ]); + + return ( +
+
{ + currentTarget.setAttribute('data-over', 'true'); + })} + onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => { + currentTarget.removeAttribute('data-over'); + })} + onDrop$={[ + sync$((e: DragEvent, currentTarget: HTMLDivElement) => { + const id = e.dataTransfer?.getData('text'); + currentTarget.dataset.droppedId = id; + currentTarget.removeAttribute('data-over'); + }), + $((_, currentTarget) => { + const id = currentTarget.dataset.droppedId; + if (id) { + const itemId = parseInt(id); + const item = [...items2.value].find((i) => i.id === itemId); + if (item) { + items2.value = items2.value.filter((i) => i.id !== itemId); + items1.value = [...items1.value, item]; + } + } + }), + ]} + > +

Container 1

+ {items1.value.map((item) => ( +
{ + const itemId = currentTarget.getAttribute('data-id'); + if (e.dataTransfer && itemId) { + e.dataTransfer?.setData('text/plain', itemId); + } + } + )} + > + {item.content} +
+ ))} +
+ +
{ + currentTarget.setAttribute('data-over', 'true'); + })} + onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => { + currentTarget.removeAttribute('data-over'); + })} + onDrop$={[ + sync$((e: DragEvent, currentTarget: HTMLDivElement) => { + const id = e.dataTransfer?.getData('text'); + currentTarget.dataset.droppedId = id; + currentTarget.removeAttribute('data-over'); + }), + $((_, currentTarget) => { + const id = currentTarget.dataset.droppedId; + if (id) { + const itemId = parseInt(id); + const item = [...items1.value].find((i) => i.id === itemId); + if (item) { + items1.value = items1.value.filter((i) => i.id !== itemId); + items2.value = [...items2.value, item]; + } + } + }), + ]} + > +

Container 2

+ {items2.value.map((item) => ( +
{ + const itemId = currentTarget.getAttribute('data-id'); + if (e.dataTransfer && itemId) { + e.dataTransfer?.setData('text/plain', itemId); + } + } + )} + > + {item.content} +
+ ))} +
+
+ ); +}); diff --git a/packages/docs/src/routes/demo/tasks/track-fn/index.tsx b/packages/docs/src/routes/demo/tasks/track-fn/index.tsx index bf5dff20df9..5af26adea89 100644 --- a/packages/docs/src/routes/demo/tasks/track-fn/index.tsx +++ b/packages/docs/src/routes/demo/tasks/track-fn/index.tsx @@ -1,5 +1,4 @@ -import { component$, useSignal, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { component$, isServer, useSignal, useTask$ } from '@qwik.dev/core'; export default component$(() => { const isUppercase = useSignal(false); diff --git a/packages/docs/src/routes/demo/tasks/track-server-guard/index.tsx b/packages/docs/src/routes/demo/tasks/track-server-guard/index.tsx index 8390416fe50..8e0704fe1f9 100644 --- a/packages/docs/src/routes/demo/tasks/track-server-guard/index.tsx +++ b/packages/docs/src/routes/demo/tasks/track-server-guard/index.tsx @@ -1,5 +1,4 @@ -import { component$, useSignal, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { component$, isServer, useSignal, useTask$ } from '@qwik.dev/core'; export default component$(() => { const text = useSignal('Initial text'); diff --git a/packages/docs/src/routes/demo/tasks/track/index.tsx b/packages/docs/src/routes/demo/tasks/track/index.tsx index 9379f40be97..b2b56e9c3c3 100644 --- a/packages/docs/src/routes/demo/tasks/track/index.tsx +++ b/packages/docs/src/routes/demo/tasks/track/index.tsx @@ -1,5 +1,4 @@ -import { component$, useSignal, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { component$, useSignal, useTask$, isServer } from '@qwik.dev/core'; export default component$(() => { const text = useSignal('Initial text'); diff --git a/packages/docs/src/routes/docs/(qwik)/components/styles/index.mdx b/packages/docs/src/routes/docs/(qwik)/components/styles/index.mdx index 7c819cb0b75..1a7036b0c3b 100644 --- a/packages/docs/src/routes/docs/(qwik)/components/styles/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/components/styles/index.mdx @@ -277,6 +277,31 @@ export const CmpStyles = component$(() => { > Notice that in order to import CSS as a string in Vite, you need to add the `?inline` query parameter to the import, like this: `import styles from './code-block.css?inline';` +`useStyles$` uses lazy loading strings that won't dynamically update. If you'd like to evaluate some JS, use a `style` tag instead + +```tsx +export const CmpStyles = component$(() => { + const primaryColor = "red"; + + // ❌ Does not work + useStyles$(` + .my-text { + --primary-color: ${primaryColor}; + } + `); + + // ✅ Adding in the style attribute works + return ( + + Some text + + ); +}); +``` + ## CSS Preprocessors diff --git a/packages/docs/src/routes/docs/(qwik)/components/tasks/index.mdx b/packages/docs/src/routes/docs/(qwik)/components/tasks/index.mdx index 12793cb7362..23eea03d669 100644 --- a/packages/docs/src/routes/docs/(qwik)/components/tasks/index.mdx +++ b/packages/docs/src/routes/docs/(qwik)/components/tasks/index.mdx @@ -194,8 +194,7 @@ During SSR, each component will wait until all tasks are completed before output ```tsx -import { component$, useSignal, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { component$, isServer, useSignal, useTask$ } from '@qwik.dev/core'; export default component$(() => { const text = useSignal('Initial text'); @@ -237,7 +236,7 @@ const delay = (time: number) => new Promise((res) => setTimeout(res, time)); > > - The `useTask$()` blocks rendering until it completes. If you don't want to block rendering, make sure that the task is resolved, and run the delay work on a separate unconnected promise. In this example, we don't await `delay()` because it would block rendering. -> Sometimes it is required to only run code either in the server or in the client. This can be achieved by using the `isServer` and `isBrowser` booleans exported from `@qwik.dev/core/build` as shown above. +> Sometimes it is required to only run code either in the server or in the client. This can be achieved by using the `isServer` and `isBrowser` booleans exported from `@qwik.dev/core` as shown above. ### `track()` as a function @@ -246,8 +245,7 @@ In the above example, `track()` was used to track a specific signal. However, `t ```tsx -import { component$, useSignal, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { component$, isServer, useSignal, useTask$ } from '@qwik.dev/core'; export default component$(() => { const isUppercase = useSignal(false); @@ -337,8 +335,7 @@ Sometimes a task needs to run only on the browser and after rendering, in that c > > > ```tsx -> import { component$, useSignal, useTask$ } from '@qwik.dev/core'; -> import { isServer } from '@qwik.dev/core/build'; +> import { component$, isServer, useSignal, useTask$ } from '@qwik.dev/core'; > > export default component$(() => { > const text = useSignal('Initial text'); diff --git a/packages/docs/src/routes/docs/(qwikrouter)/advanced/content-security-policy/index.mdx b/packages/docs/src/routes/docs/(qwikrouter)/advanced/content-security-policy/index.mdx index 685f5a84e2f..a0a05a5f559 100644 --- a/packages/docs/src/routes/docs/(qwikrouter)/advanced/content-security-policy/index.mdx +++ b/packages/docs/src/routes/docs/(qwikrouter)/advanced/content-security-policy/index.mdx @@ -48,7 +48,7 @@ src/ ```typescript title="src/routes/plugin@csp.ts" import type { RequestHandler } from "@qwik.dev/router"; -import { isDev } from "@qwik.dev/core/build"; +import { isDev } from "@qwik.dev/core"; export const onRequest: RequestHandler = event => { if (isDev) return; // Will not return CSP headers in dev mode @@ -79,7 +79,7 @@ import { ServiceWorkerRegister, } from "@qwik.dev/router"; import { RouterHead } from "./components/router-head/router-head"; -import { isDev } from "@qwik.dev/core/build"; +import { isDev } from "@qwik.dev/core"; import "./global.css"; diff --git a/packages/docs/src/routes/docs/(qwikrouter)/guides/qwik-nutshell/index.mdx b/packages/docs/src/routes/docs/(qwikrouter)/guides/qwik-nutshell/index.mdx index 872fa6c3609..f6ac17d7c44 100644 --- a/packages/docs/src/routes/docs/(qwikrouter)/guides/qwik-nutshell/index.mdx +++ b/packages/docs/src/routes/docs/(qwikrouter)/guides/qwik-nutshell/index.mdx @@ -615,8 +615,7 @@ Since Qwik executes the same code on the server and in the browser, you cannot u If you want to access browser APIs, such as `window`, `document`, `localStorage`, `sessionStorage`, `webgl` etc, you need to check if the code is running in the browser before accessing the browser APIs. ```tsx -import { component$, useTask$, useVisibleTask$, useSignal } from '@qwik.dev/core'; -import { isBrowser } from '@qwik.dev/core/build'; +import { component$, isBrowser, useTask$, useVisibleTask$, useSignal } from '@qwik.dev/core'; export default component$(() => { const ref = useSignal(); @@ -667,7 +666,7 @@ Sometimes you need to run code only on the server, such as fetching data or acce ```tsx import { component$, useTask$ } from '@qwik.dev/core'; import { server$, routeLoader$ } from '@qwik.dev/router'; -import { isServer } from '@qwik.dev/core/build'; +import { isServer } from '@qwik.dev/core'; export const useGetProducts = routeLoader$((requestEvent) => { @@ -729,29 +728,15 @@ The `server$()` is a special way to declare functions that only run on the serve ### `isServer` & `isBrowser` conditionals -Instead of `if(typeof window !== 'undefined')`, it is recommended to use the `isServer` and `isBrowser` boolean helpers exported from `@qwik.dev/core/build` to ensure your code only runs in the browser. They contain slightly more robust checks to better detect the browser environment. - -Here is the source code for reference: - -```tsx -export const isBrowser: boolean = /*#__PURE__*/ (() => - typeof window !== 'undefined' && - typeof HTMLElement !== 'undefined' && - !!window.document && - String(HTMLElement).includes('[native code]'))(); - -export const isServer: boolean = !isBrowser; -``` - -Here is how you import these for reference: +Instead of `if(typeof window !== 'undefined')`, it is recommended to use the `isServer` and `isBrowser` boolean helpers exported from `@qwik.dev/core` to ensure your code only runs on the server or in the browser. These are replaced at build time with the correct value. ```tsx -import {isServer, isBrowser} from '@qwik.dev/core/build'; +import {isServer} from '@qwik.dev/core'; // inside component$ useTask$(({ track }) => { - track(() => interactionSig.value) <-- tracks on the client when a signal has changed. + track(interactionSig) // <-- tracks on the client when a signal has changed. // server code diff --git a/packages/docs/src/routes/docs/cookbook/drag&drop/index.mdx b/packages/docs/src/routes/docs/cookbook/drag&drop/index.mdx new file mode 100644 index 00000000000..5e461f0a6b4 --- /dev/null +++ b/packages/docs/src/routes/docs/cookbook/drag&drop/index.mdx @@ -0,0 +1,358 @@ +--- +title: Cookbook | Drag & Drop +contributors: + - byte-barista +--- + +import CodeSandbox, {CodeFile} from '../../../../components/code-sandbox/index.tsx'; + + +# Drag & Drop + +Building drag-and-drop functionality is a pretty common task in web development. With Qwik, you can easily implement drag-and-drop functionality by using the `onDragStart$`, `onDragOver$`, `onDragLeave$`, and `onDrop$` APIs. You need to have in mind that Qwik processes events asynchronously. This means that some APIs such as `event.preventDefault()`, `e.dataTransfer.getData()` or `e.dataTransfer.setData()` do not work as expected. + +To work around this limitations, Qwik provides a [sync$()](/docs/cookbook/sync-events) API which allows you to process events synchronously. For preventing the default behavior, +you can use the `preventdefault:dragover` and `preventdefault:drop` attributes. + + +### Basic example + + + + + + +```tsx +import { component$, useSignal, sync$, $ } from '@builder.io/qwik'; + +export default component$(() => { + const items1 = useSignal([ + { id: 1, content: '📱 Phone' }, + { id: 2, content: '💻 Laptop' }, + { id: 3, content: '🎧 Headphones' }, + ]); + + const items2 = useSignal([ + { id: 4, content: '⌚️ Watch' }, + { id: 5, content: '🖱 Mouse' }, + { id: 6, content: '⌨️ Keyboard' }, + ]); + + return ( +
+
{ + currentTarget.setAttribute('data-over', 'true'); + })} + onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => { + currentTarget.removeAttribute('data-over'); + })} + onDrop$={[ + sync$((e: DragEvent, currentTarget: HTMLDivElement) => { + const id = e.dataTransfer?.getData('text'); + currentTarget.dataset.droppedId = id; + currentTarget.removeAttribute('data-over'); + }), + $((_, currentTarget) => { + const id = currentTarget.dataset.droppedId; + if (id) { + const itemId = parseInt(id); + const item = [...items2.value].find((i) => i.id === itemId); + if (item) { + items2.value = items2.value.filter((i) => i.id !== itemId); + items1.value = [...items1.value, item]; + } + } + }), + ]} + > +

Container 1

+ {items1.value.map((item) => ( +
{ + const itemId = currentTarget.getAttribute('data-id'); + if (e.dataTransfer && itemId) { + e.dataTransfer?.setData('text/plain', itemId); + } + } + )} + > + {item.content} +
+ ))} +
+ +
{ + currentTarget.setAttribute('data-over', 'true'); + })} + onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => { + currentTarget.removeAttribute('data-over'); + })} + onDrop$={[ + sync$((e: DragEvent, currentTarget: HTMLDivElement) => { + const id = e.dataTransfer?.getData('text'); + currentTarget.dataset.droppedId = id; + currentTarget.removeAttribute('data-over'); + }), + $((_, currentTarget) => { + const id = currentTarget.dataset.droppedId; + if (id) { + const itemId = parseInt(id); + const item = [...items1.value].find((i) => i.id === itemId); + if (item) { + items1.value = items1.value.filter((i) => i.id !== itemId); + items2.value = [...items2.value, item]; + } + } + }), + ]} + > +

Container 2

+ {items2.value.map((item) => ( +
{ + const itemId = currentTarget.getAttribute('data-id'); + if (e.dataTransfer && itemId) { + e.dataTransfer?.setData('text/plain', itemId); + } + } + )} + > + {item.content} +
+ ))} +
+
+ ); +}); +``` +
+ + +### Advanced example with sorting + + + + + + + +```tsx +import { component$, sync$, useSignal, $ } from '@builder.io/qwik'; + +type Item = { + id: number; + content: string; +}; + +export default component$(() => { + const items1 = useSignal([ + { id: 1, content: '📱 Phone' }, + { id: 2, content: '💻 Laptop' }, + { id: 3, content: '🎧 Headphones' }, + ]); + + const items2 = useSignal([ + { id: 4, content: '⌚️ Watch' }, + { id: 5, content: '🖱 Mouse' }, + { id: 6, content: '⌨️ Keyboard' }, + ]); + + return ( +
+
{ + currentTarget.setAttribute('data-over', 'true'); + })} + onDragLeave$={sync$((_: DragEvent, currentTarget: HTMLDivElement) => { + currentTarget.removeAttribute('data-over'); + })} + onDrop$={[ + sync$((e: DragEvent, currentTarget: HTMLDivElement) => { + const id = e.dataTransfer?.getData('text/plain'); + currentTarget.dataset.droppedId = id; + currentTarget.removeAttribute('data-over'); + }), + $((e, currentTarget) => { + const draggedElementId = currentTarget.dataset.droppedId; + const isDropZone = currentTarget.hasAttribute('data-dropzone'); + + if (draggedElementId) { + const itemId = parseInt(draggedElementId); + const item = items2.value.find((i) => i.id === itemId); + + if (item && isDropZone) { + items2.value = items2.value.filter((i) => i.id !== itemId); + items1.value = [...items1.value, item]; + } else { + const newItems = [...items1.value]; + const targetId = parseInt( + (e.target as HTMLDivElement).dataset.id || '0' + ); + if (targetId === 0) return; + + const targetIndex = items1.value.findIndex( + (i) => i.id === targetId + ); + const draggedIndex = items1.value.findIndex( + (i) => i.id === itemId + ); + + if (draggedIndex !== -1) { + // Sorting in the same container + swapElements(newItems, draggedIndex, targetIndex); + items1.value = newItems; + } else { + // Sorting between containers + if (!item) return; + items2.value = items2.value.filter((i) => i.id !== itemId); + insertElement(newItems, targetIndex, item); + items1.value = newItems; + } + } + } + }), + ]} + > +

Container 1

+ {items1.value.map((item) => ( +
{ + const itemId = currentTarget.getAttribute('data-id'); + if (e.dataTransfer && itemId) { + e.dataTransfer.setData('text/plain', itemId); + } + } + )} + > + {item.content} +
+ ))} +
+ +
{ + currentTarget.setAttribute('data-over', 'true'); + }} + onDragLeave$={[ + sync$((_: DragEvent, currentTarget: HTMLDivElement) => { + currentTarget.removeAttribute('data-over'); + }), + ]} + onDrop$={[ + sync$((e: DragEvent, currentTarget: HTMLDivElement) => { + const id = e.dataTransfer?.getData('text/plain'); + currentTarget.dataset.droppedId = id; + currentTarget.removeAttribute('data-over'); + }), + $((e, currentTarget) => { + const draggedElementId = currentTarget.dataset.droppedId; + const isDropZone = currentTarget.hasAttribute('data-dropzone'); + + if (draggedElementId) { + const itemId = parseInt(draggedElementId); + const item = items1.value.find((i) => i.id === itemId); + + if (isDropZone && item) { + items1.value = items1.value.filter((i) => i.id !== itemId); + items2.value = [...items2.value, item]; + } else { + const targetId = parseInt( + (e.target as HTMLDivElement).dataset.id || '0' + ); + if (targetId === 0) return; + const newItems = [...items2.value]; + const draggedIndex = items2.value.findIndex( + (i) => i.id === itemId + ); + const targetIndex = items2.value.findIndex( + (i) => i.id === targetId + ); + if (draggedIndex !== -1) { + // Sorting in the same container + swapElements(newItems, targetIndex, draggedIndex); + items2.value = newItems; + } else { + // Sorting between containers + if (!item) return; + items1.value = items1.value.filter((i) => i.id !== itemId); + insertElement(newItems, targetIndex, item); + items2.value = newItems; + } + } + } + }), + ]} + > +

Container 2

+ {items2.value.map((item) => ( +
{ + const itemId = currentTarget.getAttribute('data-id'); + if (e.dataTransfer && itemId) { + e.dataTransfer.setData('text/plain', itemId); + } + } + )} + > + {item.content} +
+ ))} +
+
+ ); +}); + +function swapElements(arr: Item[], index1: number, index2: number) { + arr[index1] = arr.splice(index2, 1, arr[index1])[0]; + + return arr; +} + +function insertElement(arr: Item[], index: number, item: Item) { + arr.splice(index, 0, item); + return arr; +} + +``` +
+ + + + +You can find more information about drag-and-drop in the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API). \ No newline at end of file diff --git a/packages/docs/src/routes/docs/cookbook/index.mdx b/packages/docs/src/routes/docs/cookbook/index.mdx index 5af20d8aaa5..bee807e0782 100644 --- a/packages/docs/src/routes/docs/cookbook/index.mdx +++ b/packages/docs/src/routes/docs/cookbook/index.mdx @@ -31,3 +31,4 @@ Examples: - [Streaming/deferred loaders](./streaming-deferred-loaders) - [Synchronous Events with State](./sync-events/) - [Theme Management](./theme-management/) +- [Drag & Drop](./drag&drop/) diff --git a/packages/docs/src/routes/docs/menu.md b/packages/docs/src/routes/docs/menu.md index f0a75029c24..b59fde45751 100644 --- a/packages/docs/src/routes/docs/menu.md +++ b/packages/docs/src/routes/docs/menu.md @@ -50,7 +50,8 @@ - [Portals](/docs/cookbook/portals/index.mdx) - [Streaming loaders](/docs/cookbook/streaming-deferred-loaders/index.mdx) - [Sync events w state](/docs/cookbook/sync-events/index.mdx) -- [Theme Managment](/docs/cookbook/theme-management/index.mdx) +- [Theme Management](/docs/cookbook/theme-management/index.mdx) +- [Drag & Drop](/docs/cookbook/drag&drop/index.mdx) ## Integrations diff --git a/packages/docs/src/routes/examples/[...id]/index!.tsx b/packages/docs/src/routes/examples/[...id]/index!.tsx index 4654f87e788..bfa366a2930 100644 --- a/packages/docs/src/routes/examples/[...id]/index!.tsx +++ b/packages/docs/src/routes/examples/[...id]/index!.tsx @@ -1,8 +1,14 @@ import exampleSections, { type ExampleApp } from '@examples-data'; +import { + component$, + isBrowser, + useStore, + useStyles$, + useTask$, + useVisibleTask$, +} from '@qwik.dev/core'; import type { PathParams, RequestHandler, StaticGenerateHandler } from '@qwik.dev/router'; -import { type DocumentHead, useLocation } from '@qwik.dev/router'; -import { component$, useStore, useStyles$, useTask$, useVisibleTask$ } from '@qwik.dev/core'; -import { isBrowser } from '@qwik.dev/core/build'; +import { useLocation, type DocumentHead } from '@qwik.dev/router'; import { Header } from '../../../components/header/header'; import { PanelToggle } from '../../../components/panel-toggle/panel-toggle'; import { Repl } from '../../../repl/repl'; diff --git a/packages/docs/src/routes/examples/apps/partial/hackernews-index/app.tsx b/packages/docs/src/routes/examples/apps/partial/hackernews-index/app.tsx index 0cda273c720..ac27535d9e6 100644 --- a/packages/docs/src/routes/examples/apps/partial/hackernews-index/app.tsx +++ b/packages/docs/src/routes/examples/apps/partial/hackernews-index/app.tsx @@ -1,5 +1,4 @@ -import { component$, useStore, useStyles$, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { component$, isServer, useStore, useStyles$, useTask$ } from '@qwik.dev/core'; import HackerNewsCSS from './hacker-news.css?inline'; export const HackerNews = component$(() => { diff --git a/packages/docs/src/routes/playground/index!.tsx b/packages/docs/src/routes/playground/index!.tsx index 1d9254e3146..d5e0785972e 100644 --- a/packages/docs/src/routes/playground/index!.tsx +++ b/packages/docs/src/routes/playground/index!.tsx @@ -1,7 +1,14 @@ import playgroundApp from '@playground-data'; +import { + $, + component$, + isBrowser, + useStore, + useStyles$, + useTask$, + useVisibleTask$, +} from '@qwik.dev/core'; import type { DocumentHead, RequestHandler } from '@qwik.dev/router'; -import { $, component$, useStore, useStyles$, useTask$, useVisibleTask$ } from '@qwik.dev/core'; -import { isBrowser } from '@qwik.dev/core/build'; import { Header } from '../../components/header/header'; import { PanelToggle } from '../../components/panel-toggle/panel-toggle'; import { Repl } from '../../repl/repl'; diff --git a/packages/docs/vite.repl-apps.ts b/packages/docs/vite.repl-apps.ts index 7b976e1c2c9..1c4f4ce9f6c 100644 --- a/packages/docs/vite.repl-apps.ts +++ b/packages/docs/vite.repl-apps.ts @@ -328,6 +328,10 @@ export function rawSource(): Plugin { if (path.startsWith('/@fs/')) { path = path.slice('/@fs'.length); } + if (path.startsWith('\x00')) { + // let's just assume it's a path + path = path.slice(1); + } if (isDev) { const devUrl = `${base}@raw-fs${path}`; return `export default "${devUrl}";`; diff --git a/packages/eslint-plugin-qwik/CHANGELOG.md b/packages/eslint-plugin-qwik/CHANGELOG.md index 11436a677b1..e7bfe3c89ce 100644 --- a/packages/eslint-plugin-qwik/CHANGELOG.md +++ b/packages/eslint-plugin-qwik/CHANGELOG.md @@ -1,5 +1,9 @@ # eslint-plugin-qwik +## 2.0.0-alpha.4 + +## 2.0.0-alpha.3 + ## 2.0.0-alpha.2 ## 2.0.0-alpha.1 diff --git a/packages/eslint-plugin-qwik/package.json b/packages/eslint-plugin-qwik/package.json index 6b1d1d2b388..400f8aa1933 100644 --- a/packages/eslint-plugin-qwik/package.json +++ b/packages/eslint-plugin-qwik/package.json @@ -1,7 +1,7 @@ { "name": "eslint-plugin-qwik", "description": "An Open-Source sub-framework designed with a focus on server-side-rendering, lazy-loading, and styling/animation.", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "author": "Qwik Team", "bugs": "https://github.com/QwikDev/qwik/issues", "dependencies": { diff --git a/packages/eslint-plugin-qwik/tests/use-method-usage/valid-inside-component.tsx b/packages/eslint-plugin-qwik/tests/use-method-usage/valid-inside-component.tsx index 6d3edc987ad..6f74c5c3bd6 100644 --- a/packages/eslint-plugin-qwik/tests/use-method-usage/valid-inside-component.tsx +++ b/packages/eslint-plugin-qwik/tests/use-method-usage/valid-inside-component.tsx @@ -1,5 +1,4 @@ -import { component$, useSignal, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { component$, isServer, useSignal, useTask$ } from '@qwik.dev/core'; export const InsideTask = component$(() => { const mySig = useSignal(0); diff --git a/packages/insights/src/db/logging.ts b/packages/insights/src/db/logging.ts index a40c19fb278..fbdf56f7c76 100644 --- a/packages/insights/src/db/logging.ts +++ b/packages/insights/src/db/logging.ts @@ -1,4 +1,4 @@ -import { isDev } from '@qwik.dev/core/build'; +import { isDev } from '@qwik.dev/core'; const LOG_TIMING: boolean = isDev; diff --git a/packages/qwik-react/CHANGELOG.md b/packages/qwik-react/CHANGELOG.md index a9fc56cba96..341b5027571 100644 --- a/packages/qwik-react/CHANGELOG.md +++ b/packages/qwik-react/CHANGELOG.md @@ -1,5 +1,19 @@ # @qwik.dev/react +## 2.0.0-alpha.4 + +### Patch Changes + +- Updated dependencies [[`8693165`](https://github.com/QwikDev/qwik/commit/86931654ce38a64d5c1730042f64989fa2a537ad), [`8693165`](https://github.com/QwikDev/qwik/commit/86931654ce38a64d5c1730042f64989fa2a537ad), [`8693165`](https://github.com/QwikDev/qwik/commit/86931654ce38a64d5c1730042f64989fa2a537ad), [`72d7c24`](https://github.com/QwikDev/qwik/commit/72d7c2450cbed380454869bab482ab6c01011221)]: + - @qwik.dev/core@2.0.0-alpha.4 + +## 2.0.0-alpha.3 + +### Patch Changes + +- Updated dependencies [[`5352f6f`](https://github.com/QwikDev/qwik/commit/5352f6fff07a2d8d0c9efc20fc95421ced06ea8e), [`9cdfc58`](https://github.com/QwikDev/qwik/commit/9cdfc58762fc19375e49c9947a1c0dd1ac0d3d2f), [`107dbc1`](https://github.com/QwikDev/qwik/commit/107dbc177e01968a53f138ea9424b6bae0834f28), [`9e4bf8f`](https://github.com/QwikDev/qwik/commit/9e4bf8f1bd03edea93725778d41a42dc36c3fc7f)]: + - @qwik.dev/core@2.0.0-alpha.3 + ## 2.0.0-alpha.2 ### Patch Changes diff --git a/packages/qwik-react/package.json b/packages/qwik-react/package.json index 769ebcd8b95..c5342ba11ca 100644 --- a/packages/qwik-react/package.json +++ b/packages/qwik-react/package.json @@ -1,7 +1,7 @@ { "name": "@qwik.dev/react", "description": "Qwik React allows adding React components into existing Qwik application", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "bugs": "https://github.com/QwikDev/qwik/issues", "devDependencies": { "@qwik.dev/core": "workspace:*", diff --git a/packages/qwik-react/src/react/qwikify.tsx b/packages/qwik-react/src/react/qwikify.tsx index 59428e735a6..97de1f2c219 100644 --- a/packages/qwik-react/src/react/qwikify.tsx +++ b/packages/qwik-react/src/react/qwikify.tsx @@ -1,19 +1,20 @@ import { - component$, - implicit$FirstArg, - type NoSerialize, - noSerialize, - type QRL, RenderOnce, SkipRender, Slot, + component$, + implicit$FirstArg, + isBrowser, + isServer, + noSerialize, useSignal, useStore, useStylesScoped$, useTask$, + type NoSerialize, + type QRL, } from '@qwik.dev/core'; -import { isBrowser, isServer } from '@qwik.dev/core/build'; import type { FunctionComponent as ReactFC } from 'react'; import type { Root } from 'react-dom/client'; import * as client from './client'; diff --git a/packages/qwik-react/src/react/server-render.tsx b/packages/qwik-react/src/react/server-render.tsx index 81c7e6465d7..77f481b9626 100644 --- a/packages/qwik-react/src/react/server-render.tsx +++ b/packages/qwik-react/src/react/server-render.tsx @@ -1,6 +1,5 @@ -import { type QRL, type Signal, Slot } from '@qwik.dev/core'; +import { Slot, isServer, type QRL, type Signal } from '@qwik.dev/core'; import { SSRComment, SSRRaw, SSRStream } from '@qwik.dev/core/internal'; -import { isServer } from '@qwik.dev/core/build'; import { renderToString } from 'react-dom/server'; import { getHostProps, getReactProps, mainExactProps } from './slot'; diff --git a/packages/qwik-react/src/react/slot.ts b/packages/qwik-react/src/react/slot.ts index f2529dcc842..3268d0b8625 100644 --- a/packages/qwik-react/src/react/slot.ts +++ b/packages/qwik-react/src/react/slot.ts @@ -1,5 +1,4 @@ -import { $, useOn, useOnDocument, useSignal } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { $, isServer, useOn, useOnDocument, useSignal } from '@qwik.dev/core'; import { Component, createContext, createElement, createRef } from 'react'; import type { QwikifyOptions, QwikifyProps } from './types'; diff --git a/packages/qwik-router/CHANGELOG.md b/packages/qwik-router/CHANGELOG.md index 33a6dc41c26..498d3db4617 100644 --- a/packages/qwik-router/CHANGELOG.md +++ b/packages/qwik-router/CHANGELOG.md @@ -1,5 +1,9 @@ # @qwik.dev/city +## 2.0.0-alpha.4 + +## 2.0.0-alpha.3 + ## 2.0.0-alpha.2 ## 2.0.0-alpha.1 diff --git a/packages/qwik-router/package.json b/packages/qwik-router/package.json index fa562768a4b..5b06a7151f6 100644 --- a/packages/qwik-router/package.json +++ b/packages/qwik-router/package.json @@ -1,7 +1,7 @@ { "name": "@qwik.dev/router", "description": "The router for Qwik.", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "bugs": "https://github.com/QwikDev/qwik/issues", "dependencies": { "@mdx-js/mdx": "^3", @@ -158,7 +158,8 @@ "types": "./lib/service-worker.d.ts", "import": "./lib/service-worker.mjs", "require": "./lib/service-worker.cjs" - } + }, + "./package.json": "./package.json" }, "files": [ "adapters", diff --git a/packages/qwik-router/src/runtime/src/client-navigate.ts b/packages/qwik-router/src/runtime/src/client-navigate.ts index 63f49ad0704..bb60ece0053 100644 --- a/packages/qwik-router/src/runtime/src/client-navigate.ts +++ b/packages/qwik-router/src/runtime/src/client-navigate.ts @@ -1,4 +1,4 @@ -import { isBrowser } from '@qwik.dev/core/build'; +import { isBrowser } from '@qwik.dev/core'; import { PREFETCHED_NAVIGATE_PATHS } from './constants'; import type { NavigationType, ScrollState } from './types'; import { isSamePath, toPath } from './utils'; diff --git a/packages/qwik-router/src/runtime/src/link-component.tsx b/packages/qwik-router/src/runtime/src/link-component.tsx index 115844cd8e0..b68f7edac2d 100644 --- a/packages/qwik-router/src/runtime/src/link-component.tsx +++ b/packages/qwik-router/src/runtime/src/link-component.tsx @@ -1,5 +1,12 @@ -import { $, component$, Slot, sync$, untrack, type QwikIntrinsicElements } from '@qwik.dev/core'; -import { isDev } from '@qwik.dev/core/build'; +import { + $, + component$, + isDev, + Slot, + sync$, + untrack, + type QwikIntrinsicElements, +} from '@qwik.dev/core'; import { prefetchSymbols } from './client-navigate'; import { loadClientData } from './use-endpoint'; import { useLocation, useNavigate } from './use-functions'; diff --git a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx index dd2de94a8c5..1f9569112de 100644 --- a/packages/qwik-router/src/runtime/src/qwik-router-component.tsx +++ b/packages/qwik-router/src/runtime/src/qwik-router-component.tsx @@ -3,6 +3,9 @@ import { $, component$, getLocale, + isBrowser, + isDev, + isServer, noSerialize, Slot, useContextProvider, @@ -20,7 +23,6 @@ import { _weakSerialize, type _ElementVNode, } from '@qwik.dev/core/internal'; -import { isBrowser, isDev, isServer } from '@qwik.dev/core/build'; import { clientNavigate } from './client-navigate'; import { CLIENT_DATA_CACHE } from './constants'; import { diff --git a/packages/qwik-router/src/runtime/src/server-functions.ts b/packages/qwik-router/src/runtime/src/server-functions.ts index e11b11db429..120d35290b8 100644 --- a/packages/qwik-router/src/runtime/src/server-functions.ts +++ b/packages/qwik-router/src/runtime/src/server-functions.ts @@ -52,7 +52,7 @@ import type { } from './types'; import { useAction, useLocation, useQwikRouterEnv } from './use-functions'; -import { isDev, isServer } from '@qwik.dev/core/build'; +import { isDev, isServer } from '@qwik.dev/core'; import type { FormSubmitCompletedDetail } from './form-component'; import { deepFreeze } from './utils'; diff --git a/packages/qwik-router/src/runtime/src/spa-init.ts b/packages/qwik-router/src/runtime/src/spa-init.ts index dcfa6917ee9..957e4c99d27 100644 --- a/packages/qwik-router/src/runtime/src/spa-init.ts +++ b/packages/qwik-router/src/runtime/src/spa-init.ts @@ -2,8 +2,7 @@ import type { ClientSPAWindow } from './qwik-router-component'; import type { ScrollHistoryState } from './scroll-restoration'; import type { ScrollState } from './types'; -import { event$ } from '@qwik.dev/core'; -import { isDev } from '@qwik.dev/core/build'; +import { event$, isDev } from '@qwik.dev/core'; // TODO Dedupe handler code from here and QwikRouterProvider? // TODO Navigation API; check for support & simplify. diff --git a/packages/qwik/CHANGELOG.md b/packages/qwik/CHANGELOG.md index d907edf377c..9e6972db073 100644 --- a/packages/qwik/CHANGELOG.md +++ b/packages/qwik/CHANGELOG.md @@ -1,5 +1,29 @@ # @qwik.dev/core +## 2.0.0-alpha.4 + +### Patch Changes + +- 🐞🩹 encode the `q:subs` property (by [@Varixo](https://github.com/Varixo) in [#7088](https://github.com/QwikDev/qwik/pull/7088)) + +- ✨ move signal invalidation to the scheduler (by [@Varixo](https://github.com/Varixo) in [#7088](https://github.com/QwikDev/qwik/pull/7088)) + +- ✨ better node attributes serialization (by [@Varixo](https://github.com/Varixo) in [#7088](https://github.com/QwikDev/qwik/pull/7088)) + +- 🐞🩹 serialize virtual props for DOM elements (by [@Varixo](https://github.com/Varixo) in [#7088](https://github.com/QwikDev/qwik/pull/7088)) + +## 2.0.0-alpha.3 + +### Patch Changes + +- 🐞🩹 prevent multiple store deserialization (by [@Varixo](https://github.com/Varixo) in [#7155](https://github.com/QwikDev/qwik/pull/7155)) + +- 🐞🩹 using ref inside useContext (by [@Varixo](https://github.com/Varixo) in [#7132](https://github.com/QwikDev/qwik/pull/7132)) + +- 🐞🩹 types error when migrating to V2 with `moduleResulution: "node"` (by [@shairez](https://github.com/shairez) in [#7159](https://github.com/QwikDev/qwik/pull/7159)) + +- 🐞🩹 replacing projection content with null or undefined (by [@Varixo](https://github.com/Varixo) in [#7148](https://github.com/QwikDev/qwik/pull/7148)) + ## 2.0.0-alpha.2 ### Patch Changes diff --git a/packages/qwik/package.json b/packages/qwik/package.json index f537102438a..ad8a08c16f7 100644 --- a/packages/qwik/package.json +++ b/packages/qwik/package.json @@ -1,7 +1,7 @@ { "name": "@qwik.dev/core", "description": "An open source framework for building instant loading web apps at any scale, without the extra effort.", - "version": "2.0.0-alpha.2", + "version": "2.0.0-alpha.4", "author": "Qwik Team", "bin": { "qwik": "./qwik-cli.cjs" @@ -134,7 +134,7 @@ "./qwikloader.debug.js": "./dist/qwikloader.debug.js", "./qwik-prefetch.js": "./dist/qwik-prefetch.js", "./qwik-prefetch.debug.js": "./dist/qwik-prefetch.debug.js", - "./package.json": "./dist/package.json" + "./package.json": "./package.json" }, "exports_annotation": "We use the build for the optimizer because esbuild doesn't like the html?raw imports in the server plugin and it's only used in the vite configs", "files": [ @@ -194,5 +194,5 @@ "build.insights": "cd src/insights && vite build --mode lib --emptyOutDir" }, "type": "module", - "types": "./dist/core.d.ts" + "types": "./public.d.ts" } diff --git a/packages/qwik/public.d.ts b/packages/qwik/public.d.ts index edc033106c7..0208f05569b 100644 --- a/packages/qwik/public.d.ts +++ b/packages/qwik/public.d.ts @@ -17,6 +17,9 @@ export { getLocale, getPlatform, implicit$FirstArg, + isBrowser, + isDev, + isServer, isSignal, jsx, JSXChildren, diff --git a/packages/qwik/src/cli/migrate-v2/replace-package.ts b/packages/qwik/src/cli/migrate-v2/replace-package.ts index 46815cf6f2a..a54fa60b5fd 100644 --- a/packages/qwik/src/cli/migrate-v2/replace-package.ts +++ b/packages/qwik/src/cli/migrate-v2/replace-package.ts @@ -9,8 +9,14 @@ function updateFileContent(path: string, content: string) { log.info(`"${path}" has been updated`); } -export function replacePackage(oldPackageName: string, newPackageName: string): void { - replacePackageInDependencies(oldPackageName, newPackageName); +export function replacePackage( + oldPackageName: string, + newPackageName: string, + skipDependencies = false +): void { + if (!skipDependencies) { + replacePackageInDependencies(oldPackageName, newPackageName); + } replaceMentions(oldPackageName, newPackageName); } diff --git a/packages/qwik/src/cli/migrate-v2/run-migration.ts b/packages/qwik/src/cli/migrate-v2/run-migration.ts index 73b0c66f240..7ded4091a3e 100644 --- a/packages/qwik/src/cli/migrate-v2/run-migration.ts +++ b/packages/qwik/src/cli/migrate-v2/run-migration.ts @@ -8,6 +8,7 @@ import { removeTsMorphFromPackageJson, updateDependencies, } from './update-dependencies'; +import { updateConfigurations } from './update-configurations'; export async function runV2Migration(app: AppCommand) { intro( @@ -40,7 +41,12 @@ export async function runV2Migration(app: AppCommand) { ], '@builder.io/qwik-city' ); + replaceImportInFiles( + [['qwikCityPlan', 'qwikRouterConfig']], + '@qwik-city-plan' // using old name, package name will be updated in the next step + ); + replacePackage('@qwik-city-plan', '@qwik-router-config', true); replacePackage('@builder.io/qwik-city', '@qwik.dev/router'); replacePackage('@builder.io/qwik-react', '@qwik.dev/react'); // "@builder.io/qwik" should be the last one because it's name is a substring of the package names above @@ -50,6 +56,8 @@ export async function runV2Migration(app: AppCommand) { await removeTsMorphFromPackageJson(); } + updateConfigurations(); + await updateDependencies(); log.success(`${green(`Your application has been successfully migrated to v2!`)}`); } catch (error) { diff --git a/packages/qwik/src/cli/migrate-v2/update-configurations.ts b/packages/qwik/src/cli/migrate-v2/update-configurations.ts new file mode 100644 index 00000000000..db8905a7406 --- /dev/null +++ b/packages/qwik/src/cli/migrate-v2/update-configurations.ts @@ -0,0 +1,20 @@ +import { readFileSync, writeFileSync } from 'fs'; +import { log } from '@clack/prompts'; + +export function updateConfigurations() { + try { + updateTsconfig(); + } catch (error) { + log.error('Failed to update tsconfig.json configuration.'); + } +} + +function updateTsconfig() { + const tsConfigPath = 'tsconfig.json'; + const tsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf-8')); + if (!tsConfig) { + return; + } + tsConfig.compilerOptions.moduleResolution = 'bundler'; + writeFileSync(tsConfigPath, JSON.stringify(tsConfig, null, 2)); +} diff --git a/packages/qwik/src/cli/migrate-v2/update-dependencies.ts b/packages/qwik/src/cli/migrate-v2/update-dependencies.ts index 6ab60ecd57d..8e6149bdd06 100644 --- a/packages/qwik/src/cli/migrate-v2/update-dependencies.ts +++ b/packages/qwik/src/cli/migrate-v2/update-dependencies.ts @@ -91,4 +91,5 @@ export async function removeTsMorphFromPackageJson() { const packageJson = await readPackageJson(process.cwd()); delete packageJson.dependencies?.['ts-morph']; delete packageJson.devDependencies?.['ts-morph']; + await writePackageJson(process.cwd(), packageJson); } diff --git a/packages/qwik/src/core/api.md b/packages/qwik/src/core/api.md index c6876244be9..95487f0d77a 100644 --- a/packages/qwik/src/core/api.md +++ b/packages/qwik/src/core/api.md @@ -5,6 +5,9 @@ ```ts import * as CSS_2 from 'csstype'; +import { isBrowser } from '@qwik.dev/core/build'; +import { isDev } from '@qwik.dev/core/build'; +import { isServer } from '@qwik.dev/core/build'; import type { StreamWriter as StreamWriter_2 } from '@qwik.dev/core'; // @public @@ -184,14 +187,12 @@ class DomContainer extends _SharedContainer implements ClientContainer { getParentHost(host: HostElement): HostElement | null; // (undocumented) getSyncFn(id: number): (...args: unknown[]) => unknown; + // Warning: (ae-forgotten-export) The symbol "HostElement" needs to be exported by the entry point index.d.ts + // // (undocumented) handleError(err: any, host: HostElement): void; // (undocumented) parseQRL(qrl: string): QRL; - // Warning: (ae-forgotten-export) The symbol "HostElement" needs to be exported by the entry point index.d.ts - // - // (undocumented) - processJsx(host: HostElement, jsx: JSXOutput): ValueOrPromise; // (undocumented) qBase: string; // (undocumented) @@ -220,10 +221,12 @@ export { DomContainer as _DomContainer } export type EagernessOptions = 'visible' | 'load' | 'idle'; // @internal (undocumented) -export class _EffectData = Record> { - constructor(data: T); +export class _EffectData { + constructor(data: NodePropData); + // Warning: (ae-forgotten-export) The symbol "NodePropData" needs to be exported by the entry point index.d.ts + // // (undocumented) - data: T; + data: NodePropData; } // @internal (undocumented) @@ -333,9 +336,15 @@ export const inlinedQrl: (symbol: T, symbolName: string, lexicalScopeCapture? // @internal (undocumented) export const inlinedQrlDEV: (symbol: T, symbolName: string, opts: QRLDev, lexicalScopeCapture?: any[]) => QRL; +export { isBrowser } + +export { isDev } + // @internal (undocumented) export const _isJSXNode: (n: unknown) => n is JSXNodeInternal; +export { isServer } + // @public (undocumented) export const isSignal: (value: any) => value is Signal; @@ -837,8 +846,6 @@ export abstract class _SharedContainer implements Container { // (undocumented) abstract handleError(err: any, $host$: HostElement): void; // (undocumented) - abstract processJsx(host: HostElement, jsx: JSXOutput): ValueOrPromise; - // (undocumented) abstract resolveContext(host: HostElement, contextId: ContextId): T | undefined; // Warning: (ae-forgotten-export) The symbol "SymbolToChunkResolver" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "SerializationContext" needs to be exported by the entry point index.d.ts @@ -858,10 +865,8 @@ export abstract class _SharedContainer implements Container { abstract setContext(host: HostElement, context: ContextId, value: T): void; // (undocumented) abstract setHostProp(host: HostElement, name: string, value: T): void; - // Warning: (ae-forgotten-export) The symbol "Effect" needs to be exported by the entry point index.d.ts - // // (undocumented) - trackSignalValue(signal: Signal, subscriber: Effect, property: string, data: _EffectData): T; + trackSignalValue(signal: Signal, subscriber: HostElement, property: string, data: _EffectData): T; } // @public diff --git a/packages/qwik/src/core/client/dom-container.ts b/packages/qwik/src/core/client/dom-container.ts index 1d7d88339f0..c7c68548eb4 100644 --- a/packages/qwik/src/core/client/dom-container.ts +++ b/packages/qwik/src/core/client/dom-container.ts @@ -4,10 +4,8 @@ import { assertTrue } from '../shared/error/assert'; import { getPlatform } from '../shared/platform/platform'; import type { QRL } from '../shared/qrl/qrl.public'; import { ERROR_CONTEXT, isRecoverable } from '../shared/error/error-handling'; -import type { JSXOutput } from '../shared/jsx/types/jsx-node'; import type { ContextId } from '../use/use-context'; import { EMPTY_ARRAY } from '../shared/utils/flyweight'; -import { throwErrorAndStop } from '../shared/utils/log'; import { ELEMENT_PROPS, ELEMENT_SEQ, @@ -30,10 +28,8 @@ import { import { isPromise } from '../shared/utils/promises'; import { isSlotProp } from '../shared/utils/prop'; import { qDev } from '../shared/utils/qdev'; -import type { ValueOrPromise } from '../shared/utils/types'; import { ChoreType } from '../shared/scheduler'; import { - addComponentStylePrefix, convertScopedStyleIdsToArray, convertStyleIdsToString, } from '../shared/utils/scoped-styles'; @@ -69,13 +65,13 @@ import { vnode_setProp, type VNodeJournal, } from './vnode'; -import { vnode_diff } from './vnode-diff'; +import { QError, qError } from '../shared/error/error'; /** @public */ export function getDomContainer(element: Element | VNode): IClientContainer { const qContainerElement = _getQContainerElement(element); if (!qContainerElement) { - throwErrorAndStop('Unable to find q:container.'); + throw qError(QError.containerNotFound); } return getDomContainerFromQContainerElement(qContainerElement!); } @@ -149,7 +145,7 @@ export class DomContainer extends _SharedContainer implements IClientContainer { ); this.qContainer = element.getAttribute(QContainerAttr)!; if (!this.qContainer) { - throwErrorAndStop("Element must have 'q:container' attribute."); + throw qError(QError.elementWithoutContainer); } this.$journal$ = [ // The first time we render we need to hoist the styles. @@ -193,12 +189,6 @@ export class DomContainer extends _SharedContainer implements IClientContainer { return inflateQRL(this, parseQRL(qrl)) as QRL; } - processJsx(host: HostElement, jsx: JSXOutput): ValueOrPromise { - // console.log('>>>> processJsx', String(host)); - const styleScopedId = this.getHostProp(host, QScopedStyle); - return vnode_diff(this, jsx, host as VirtualVNode, addComponentStylePrefix(styleScopedId)); - } - handleError(err: any, host: HostElement): void { if (qDev) { // Clean vdom diff --git a/packages/qwik/src/core/client/dom-render.ts b/packages/qwik/src/core/client/dom-render.ts index ec54f13d37c..4b3f1f0ff09 100644 --- a/packages/qwik/src/core/client/dom-render.ts +++ b/packages/qwik/src/core/client/dom-render.ts @@ -6,6 +6,8 @@ import { DomContainer, getDomContainer } from './dom-container'; import { cleanup } from './vnode-diff'; import { QContainerAttr } from '../shared/utils/markers'; import type { RenderOptions, RenderResult } from './types'; +import { qDev } from '../shared/utils/qdev'; +import { QError, qError } from '../shared/error/error'; /** * Render JSX. @@ -32,6 +34,9 @@ export const render = async ( } parent = child as Element; } + if (qDev && parent.hasAttribute(QContainerAttr)) { + throw qError(QError.cannotRenderOverExistingContainer, [parent]); + } (parent as Element).setAttribute(QContainerAttr, QContainerValue.RESUMED); const container = getDomContainer(parent as HTMLElement) as DomContainer; diff --git a/packages/qwik/src/core/client/vnode-diff.ts b/packages/qwik/src/core/client/vnode-diff.ts index ad9799b5087..d7565a4b622 100644 --- a/packages/qwik/src/core/client/vnode-diff.ts +++ b/packages/qwik/src/core/client/vnode-diff.ts @@ -14,7 +14,7 @@ import { Slot } from '../shared/jsx/slot.public'; import type { JSXNodeInternal, JSXOutput } from '../shared/jsx/types/jsx-node'; import type { JSXChildren } from '../shared/jsx/types/jsx-qwik-attributes'; import { SSRComment, SSRRaw, SkipRender } from '../shared/jsx/utils.public'; -import { trackSignal, untrack } from '../use/use-core'; +import { trackSignalAndAssignHost, untrack } from '../use/use-core'; import { TaskFlags, cleanupTask, isTask } from '../use/use-task'; import { EMPTY_OBJ } from '../shared/utils/flyweight'; import { @@ -27,7 +27,9 @@ import { QSlot, QSlotParent, QStyle, + QSubscribers, QTemplate, + Q_PREFIX, dangerouslySetInnerHTML, } from '../shared/utils/markers'; import { isPromise } from '../shared/utils/promises'; @@ -39,7 +41,7 @@ import { isHtmlAttributeAnEventName, isJsxPropertyAnEventName, } from '../shared/utils/event-names'; -import { ChoreType, type NodePropData } from '../shared/scheduler'; +import { ChoreType } from '../shared/scheduler'; import { hasClassAttr } from '../shared/utils/scoped-styles'; import type { HostElement, QElement, QwikLoaderEventScope, qWindow } from '../shared/types'; import { DEBUG_TYPE, QContainerValue, VirtualType } from '../shared/types'; @@ -91,7 +93,7 @@ import { type VNodeJournal, } from './vnode'; import { getNewElementNamespaceData } from './vnode-namespace'; -import { WrappedSignal, EffectProperty, isSignal, EffectData } from '../signal/signal'; +import { WrappedSignal, EffectProperty, isSignal, EffectPropData } from '../signal/signal'; import type { Signal } from '../signal/signal.public'; import { executeComponent } from '../shared/component-execution'; import { isParentSlotProp, isSlotProp } from '../shared/utils/prop'; @@ -100,8 +102,8 @@ import { clearSubscriberEffectDependencies, clearVNodeEffectDependencies, } from '../signal/signal-subscriber'; -import { throwErrorAndStop } from '../shared/utils/log'; import { serializeAttribute } from '../shared/utils/styles'; +import { QError, qError } from '../shared/error/error'; export type ComponentQueue = Array; @@ -193,12 +195,12 @@ export const vnode_diff = ( descend(jsxValue, false); } else if (isSignal(jsxValue)) { if (vCurrent) { - clearVNodeEffectDependencies(vCurrent); + clearVNodeEffectDependencies(container, vCurrent); } expectVirtual(VirtualType.WrappedSignal, null); descend( - trackSignal( - () => (jsxValue as Signal).value, + trackSignalAndAssignHost( + jsxValue as Signal, (vNewNode || vCurrent)!, EffectProperty.VNODE, container @@ -402,51 +404,52 @@ export const vnode_diff = ( ///////////////////////////////////////////////////////////////////////////// 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 projectionChildren = Array.isArray(children) ? children : [children]; + 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)); - } + 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]; - const slotName = String( - (isJSXNode(child) && directGetPropsProxyProp(child, QSlot)) || QDefaultSlot - ); - const idx = mapApp_findIndx(projections, slotName, 0); - let jsxBucket: JSXNodeImpl; - if (idx >= 0) { - jsxBucket = projections[idx + 1] as any; - } else { - projections.splice(~idx, 0, slotName, (jsxBucket = createProjectionJSXNode(slotName))); - } - const removeProjection = child === false; - if (!removeProjection) { - (jsxBucket.children as JSXChildren[]).push(child); - } + if (projections.length === 0 && children == null) { + // We did not find any existing slots and we don't have any children to project. + return; + } + + /// STEP 1: Bucketize the children based on the projection name. + for (let i = 0; i < projectionChildren.length; i++) { + const child = projectionChildren[i]; + const slotName = String( + (isJSXNode(child) && directGetPropsProxyProp(child, QSlot)) || QDefaultSlot + ); + const idx = mapApp_findIndx(projections, slotName, 0); + let jsxBucket: JSXNodeImpl; + if (idx >= 0) { + jsxBucket = projections[idx + 1] as any; + } else { + projections.splice(~idx, 0, slotName, (jsxBucket = createProjectionJSXNode(slotName))); } - /// STEP 2: remove the names - for (let i = projections.length - 2; i >= 0; i = i - 2) { - projections.splice(i, 1); + const removeProjection = child === false; + if (!removeProjection) { + (jsxBucket.children as JSXChildren[]).push(child); } - descend(projections, true); } + /// STEP 2: remove the names + for (let i = projections.length - 2; i >= 0; i = i - 2) { + projections.splice(i, 1); + } + descend(projections, true); } function expectProjection() { @@ -526,7 +529,7 @@ export const vnode_diff = ( if (constProps && typeof constProps == 'object' && 'name' in constProps) { const constValue = constProps.name; if (vHost && constValue instanceof WrappedSignal) { - return trackSignal(() => constValue.value, vHost, EffectProperty.COMPONENT, container); + return trackSignalAndAssignHost(constValue, vHost, EffectProperty.COMPONENT, container); } } return directGetPropsProxyProp(jsxNode, 'name') || QDefaultSlot; @@ -630,12 +633,12 @@ export const vnode_diff = ( } if (isSignal(value)) { - const signalData = new EffectData({ + const signalData = new EffectPropData({ $scopedStyleIdPrefix$: scopedStyleIdPrefix, $isConst$: true, }); - value = trackSignal( - () => (value as Signal).value, + value = trackSignalAndAssignHost( + value as Signal, vNewNode as ElementVNode, key, container, @@ -652,7 +655,7 @@ export const vnode_diff = ( if (elementName === 'textarea' && key === 'value') { if (typeof value !== 'string') { if (isDev) { - throwErrorAndStop('The value of the textarea must be a string'); + throw qError(QError.wrongTextareaValue); } continue; } @@ -817,7 +820,7 @@ export const vnode_diff = ( }; while (srcKey !== null || dstKey !== null) { - if (dstKey?.startsWith(HANDLER_PREFIX) || dstKey == ELEMENT_KEY) { + if (dstKey?.startsWith(HANDLER_PREFIX) || dstKey?.startsWith(Q_PREFIX)) { // These are a special keys which we use to mark the event handlers as immutable or // element key we need to ignore them. dstIdx++; // skip the destination value, we don't care about it. @@ -1036,7 +1039,7 @@ export const vnode_diff = ( container.$scheduler$(ChoreType.COMPONENT, host, componentQRL, jsxProps); } } - jsxNode.children != null && descendContentToProject(jsxNode.children, host); + descendContentToProject(jsxNode.children, host); } else { const lookupKey = jsxNode.key; const vNodeLookupKey = getKey(host); @@ -1086,7 +1089,7 @@ export const vnode_diff = ( jsxProps: Props ) { if (host) { - clearVNodeEffectDependencies(host); + clearVNodeEffectDependencies(container, host); } vnode_insertBefore( journal, @@ -1203,8 +1206,8 @@ function propsDiffer(src: Record, dst: Record): boolea if (!src || !dst) { return true; } - let srcKeys = removeChildrenKey(Object.keys(src)); - let dstKeys = removeChildrenKey(Object.keys(dst)); + let srcKeys = removePropsKeys(Object.keys(src), ['children', QSubscribers]); + let dstKeys = removePropsKeys(Object.keys(dst), ['children', QSubscribers]); if (srcKeys.length !== dstKeys.length) { return true; } @@ -1220,11 +1223,15 @@ function propsDiffer(src: Record, dst: Record): boolea return false; } -function removeChildrenKey(keys: string[]): string[] { - const childrenIdx = keys.indexOf('children'); - if (childrenIdx !== -1) { - keys.splice(childrenIdx, 1); +function removePropsKeys(keys: string[], propKeys: string[]): string[] { + for (let i = propKeys.length - 1; i >= 0; i--) { + const propKey = propKeys[i]; + const propIdx = keys.indexOf(propKey); + if (propIdx !== -1) { + keys.splice(propIdx, 1); + } } + return keys; } @@ -1249,11 +1256,10 @@ export function cleanup(container: ClientContainer, vNode: VNode) { do { const type = vCursor[VNodeProps.flags]; if (type & VNodeFlags.ELEMENT_OR_VIRTUAL_MASK) { + clearVNodeEffectDependencies(container, vCursor); + markVNodeAsDeleted(vCursor); // Only elements and virtual nodes need to be traversed for children if (type & VNodeFlags.Virtual) { - // Only virtual nodes have subscriptions - clearVNodeEffectDependencies(vCursor); - markVNodeAsDeleted(vCursor); const seq = container.getHostProp>(vCursor as VirtualVNode, ELEMENT_SEQ); if (seq) { for (let i = 0; i < seq.length; i++) { diff --git a/packages/qwik/src/core/client/vnode.ts b/packages/qwik/src/core/client/vnode.ts index 52eaaa4b069..9cd37855f77 100644 --- a/packages/qwik/src/core/client/vnode.ts +++ b/packages/qwik/src/core/client/vnode.ts @@ -121,7 +121,6 @@ import { isDev } from '@qwik.dev/core/build'; import { qwikDebugToString } from '../debug'; import { assertDefined, assertEqual, assertFalse, assertTrue } from '../shared/error/assert'; import { isText } from '../shared/utils/element'; -import { throwErrorAndStop } from '../shared/utils/log'; import { ELEMENT_ID, ELEMENT_KEY, @@ -142,6 +141,7 @@ import { QSlotRef, QStyle, QStylesAllSelector, + QSubscribers, Q_PROPS_SEPARATOR, dangerouslySetInnerHTML, } from '../shared/utils/markers'; @@ -169,6 +169,7 @@ import { vnode_getElementNamespaceFlags, } from './vnode-namespace'; import { escapeHTML } from '../shared/utils/character-escaping'; +import { QError, qError } from '../shared/error/error'; ////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -730,14 +731,7 @@ export const vnode_getVNodeForChildNode = ( ensureElementVNode(vNode); let child = vnode_getFirstChild(vNode); assertDefined(child, 'Missing child.'); - // console.log( - // 'SEARCHING', - // child[VNodeProps.flags], - // child[VNodeProps.node]?.outerHTML, - // childNode.outerHTML - // ); while (child && child[ElementVNodeProps.element] !== childElement) { - // console.log('CHILD', child[VNodeProps.node]?.outerHTML, childNode.outerHTML); if (vnode_isVirtualVNode(child)) { const next = vnode_getNextSibling(child); const firstChild = vnode_getFirstChild(child); @@ -1235,13 +1229,58 @@ export const vnode_materialize = (vNode: ElementVNode) => { const element = vNode[ElementVNodeProps.element]; const firstChild = fastFirstChild(element); const vNodeData = (element.ownerDocument as QDocument)?.qVNodeData?.get(element); - const vFirstChild = vNodeData - ? materializeFromVNodeData(vNode, vNodeData, element, firstChild) - : materializeFromDOM(vNode, firstChild); + + const vFirstChild = materialize(vNode, element, firstChild, vNodeData); return vFirstChild; }; -const ensureMaterialized = (vnode: ElementVNode): VNode | null => { +const materialize = ( + vNode: ElementVNode, + element: Element, + firstChild: Node | null, + vNodeData?: string +): VNode | null => { + if (vNodeData) { + if (vNodeData.charCodeAt(0) === VNodeDataChar.SEPARATOR) { + /** + * If vNodeData start with the `VNodeDataChar.SEPARATOR` then it means that the vNodeData + * contains some data for DOM element. We need to split it to DOM element vNodeData and + * virtual element vNodeData. + * + * For example `|=6`4|2{J=7`3|q:type|S}` should split into `=6`4`and`2{J=7`3|q:type|S}`, where + * `=6`4` is vNodeData for the DOM element. + */ + + const elementVNodeDataStartIdx = 1; + let elementVNodeDataEndIdx = 1; + while (vNodeData.charCodeAt(elementVNodeDataEndIdx) !== VNodeDataChar.SEPARATOR) { + elementVNodeDataEndIdx++; + } + const elementVNodeData = vNodeData.substring( + elementVNodeDataStartIdx, + elementVNodeDataEndIdx + ); + + // Override vNodeData variable for materializing a virtual element + vNodeData = vNodeData.substring(elementVNodeDataEndIdx + 1); + + // Materialize DOM element from HTML. If the `vNodeData` is not empty, + // then also materialize virtual element from vNodeData + const vFirstChild = materializeFromDOM(vNode, firstChild, elementVNodeData); + if (!vNodeData) { + // If it is empty then we don't need to call the `materializeFromVNodeData`. + return vFirstChild; + } + } + // Materialize virtual element form vNodeData + return materializeFromVNodeData(vNode, vNodeData, element, firstChild); + } else { + // Materialize DOM element from HTML only + return materializeFromDOM(vNode, firstChild); + } +}; + +export const ensureMaterialized = (vnode: ElementVNode): VNode | null => { const vParent = ensureElementVNode(vnode); let vFirstChild = vParent[ElementVNodeProps.firstChild]; if (vFirstChild === undefined) { @@ -1387,7 +1426,7 @@ const isQStyleElement = (node: Node | null): node is Element => { ); }; -const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null) => { +const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null, vData?: string) => { let vFirstChild: VNode | null = null; const skipStyleElements = () => { @@ -1423,9 +1462,78 @@ const materializeFromDOM = (vParent: ElementVNode, firstChild: Node | null) => { } vParent[ElementVNodeProps.lastChild] = vChild || null; vParent[ElementVNodeProps.firstChild] = vFirstChild; + + if (vData) { + /** + * If we need to materialize from DOM and we have vNodeData it means that we have some virtual + * props for that node. + */ + let container: ClientContainer | null = null; + processVNodeData(vData, (peek, consumeValue) => { + if (peek() === VNodeDataChar.ID) { + if (!container) { + container = getDomContainer(vParent[ElementVNodeProps.element]); + } + const id = consumeValue(); + container.$setRawState$(parseInt(id), vParent); + isDev && vnode_setAttr(null, vParent, ELEMENT_ID, id); + } else if (peek() === VNodeDataChar.SUBS) { + vnode_setProp(vParent, QSubscribers, consumeValue()); + } else { + // prevent infinity loop if there are some characters outside the range + consumeValue(); + } + }); + } + return vFirstChild; }; +const processVNodeData = ( + vData: string, + callback: ( + peek: () => number, + consumeValue: () => string, + consume: () => number, + nextToConsumeIdx: number + ) => void +) => { + let nextToConsumeIdx = 0; + let ch = 0; + let peekCh = 0; + const peek = () => { + if (peekCh !== 0) { + return peekCh; + } else { + return (peekCh = nextToConsumeIdx < vData.length ? vData.charCodeAt(nextToConsumeIdx) : 0); + } + }; + const consume = () => { + ch = peek(); + peekCh = 0; + nextToConsumeIdx++; + return ch; + }; + + const consumeValue = () => { + consume(); + const start = nextToConsumeIdx; + while ( + (peek() <= 58 /* `:` */ && peekCh !== 0) || + peekCh === 95 /* `_` */ || + (peekCh >= 65 /* `A` */ && peekCh <= 90) /* `Z` */ || + (peekCh >= 97 /* `a` */ && peekCh <= 122) /* `z` */ + ) { + consume(); + } + return vData.substring(start, nextToConsumeIdx); + }; + + while (peek() !== 0) { + callback(peek, consumeValue, consume, nextToConsumeIdx); + } +}; + export const vnode_getNextSibling = (vnode: VNode): VNode | null => { return vnode[VNodeProps.nextSibling]; }; @@ -1529,7 +1637,7 @@ export const vnode_getPropStartIndex = (vnode: VNode): number => { } else if (type === VNodeFlags.Virtual) { return VirtualVNodeProps.PROPS_OFFSET; } - throw throwErrorAndStop('Invalid vnode type.'); + throw qError(QError.invalidVNodeType, [type]); }; export const vnode_propsToRecord = (vnode: VNode): Record => { @@ -1645,25 +1753,10 @@ function materializeFromVNodeData( child: Node | null ): VNode { let idx = 0; - let nextToConsumeIdx = 0; let vFirst: VNode | null = null; let vLast: VNode | null = null; let previousTextNode: TextVNode | null = null; - let ch = 0; - let peekCh = 0; - const peek = () => { - if (peekCh !== 0) { - return peekCh; - } else { - return (peekCh = nextToConsumeIdx < vData!.length ? vData!.charCodeAt(nextToConsumeIdx) : 0); - } - }; - const consume = () => { - ch = peek(); - peekCh = 0; - nextToConsumeIdx++; - return ch; - }; + const addVNode = (node: VNode) => { node[VNodeProps.flags] = (node[VNodeProps.flags] & VNodeFlagsIndex.negated_mask) | (idx << VNodeFlagsIndex.shift); @@ -1677,37 +1770,17 @@ function materializeFromVNodeData( vLast = node; }; - const consumeValue = () => { - consume(); - const start = nextToConsumeIdx; - while ( - (peek() <= 58 /* `:` */ && peekCh !== 0) || - peekCh === 95 /* `_` */ || - (peekCh >= 65 /* `A` */ && peekCh <= 90) /* `Z` */ || - (peekCh >= 97 /* `a` */ && peekCh <= 122) /* `z` */ - ) { - consume(); - } - return vData.substring(start, nextToConsumeIdx); - }; - let textIdx = 0; let combinedText: string | null = null; let container: ClientContainer | null = null; - // console.log( - // 'processVNodeData', - // vNodeData, - // (child?.parentNode as HTMLElement | undefined)?.outerHTML - // ); - while (peek() !== 0) { + + processVNodeData(vData, (peek, consumeValue, consume, nextToConsumeIdx) => { if (isNumber(peek())) { // Element counts get encoded as numbers. while (!isElement(child)) { child = fastNextSibling(child); if (!child) { - throwErrorAndStop( - 'Materialize error: missing element: ' + vData + ' ' + peek() + ' ' + nextToConsumeIdx - ); + throw qError(QError.materializeVNodeDataError, [vData, peek(), nextToConsumeIdx]); } } // We pretend that style element's don't exist as they can get moved out. @@ -1749,6 +1822,8 @@ function materializeFromVNodeData( 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.SUBS) { + vnode_setProp(vParent, QSubscribers, consumeValue()); } else if (peek() === VNodeDataChar.CONTEXT) { vnode_setAttr(null, vParent, QCtxAttr, consumeValue()); } else if (peek() === VNodeDataChar.OPEN) { @@ -1794,7 +1869,7 @@ function materializeFromVNodeData( textIdx += length; // Text nodes get encoded as alphanumeric characters. } - } + }); vParent[ElementVNodeProps.lastChild] = vLast; return vFirst!; } @@ -1808,7 +1883,7 @@ export const vnode_getType = (vnode: VNode): 1 | 3 | 11 => { } else if (type & VNodeFlags.Text) { return 3 /* Text */; } - throw throwErrorAndStop('Unknown vnode type: ' + type); + throw qError(QError.invalidVNodeType, [type]); }; const isElement = (node: any): node is Element => diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index d5050e8d2b4..9ae8ab7acf2 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -5,6 +5,8 @@ export { componentQrl, component$ } from './shared/component.public'; export type { PropsOf, OnRenderFn, Component, PublicProps } from './shared/component.public'; +export { isBrowser, isDev, isServer } from '@qwik.dev/core/build'; + ////////////////////////////////////////////////////////////////////////////////////////// // Developer Event API ////////////////////////////////////////////////////////////////////////////////////////// @@ -131,7 +133,7 @@ export { useErrorBoundary } from './use/use-error-boundary'; export type { ErrorBoundaryStore } from './shared/error/error-handling'; export { type ReadonlySignal, type Signal, type ComputedSignal } from './signal/signal.public'; export { isSignal, createSignal, createComputedQrl, createComputed$ } from './signal/signal.public'; -export { EffectData as _EffectData } from './signal/signal'; +export { EffectPropData as _EffectData } from './signal/signal'; ////////////////////////////////////////////////////////////////////////////////////////// // Developer Low-Level API diff --git a/packages/qwik/src/core/shared/component-execution.ts b/packages/qwik/src/core/shared/component-execution.ts index ca53bf3a1ec..42bb4ac7759 100644 --- a/packages/qwik/src/core/shared/component-execution.ts +++ b/packages/qwik/src/core/shared/component-execution.ts @@ -93,7 +93,7 @@ export const executeComponent = ( } if (vnode_isVNode(renderHost)) { - clearVNodeEffectDependencies(renderHost); + clearVNodeEffectDependencies(container, renderHost); } return componentFn(props); diff --git a/packages/qwik/src/core/shared/error/error.ts b/packages/qwik/src/core/shared/error/error.ts index e73a7e5f222..841e56da8e9 100644 --- a/packages/qwik/src/core/shared/error/error.ts +++ b/packages/qwik/src/core/shared/error/error.ts @@ -6,38 +6,57 @@ export const codeToText = (code: number, ...parts: any[]): string => { // Keep one error, one line to make it easier to search for the error message. const MAP = [ 'Error while serializing class or style attributes', // 0 - 'Can not serialize a HTML Node that is not an Element', // 1 - 'Runtime but no instance found on element.', // 2 - 'Only primitive and object literals can be serialized', // 3 - 'Crash while rendering', // 4 + '', // 1 unused + '', // 2 unused + 'Only primitive and object literals can be serialized. {{0}}', // 3 + '', // 4 unused 'You can render over a existing q:container. Skipping render().', // 5 - 'Set property {{0}}', // 6 - "Only function's and 'string's are supported.", // 7 - "Only objects can be wrapped in 'QObject'", // 8 - `Only objects literals can be wrapped in 'QObject'`, // 9 + '', // 6 unused + '', // 7 unused + '', // 8 unused + '', // 9 unused 'QRL is not a function', // 10 'Dynamic import not found', // 11 'Unknown type argument', // 12 `Actual value for useContext({{0}}) can not be found, make sure some ancestor component has set a value using useContextProvider(). In the browser make sure that the context was used during SSR so its state was serialized.`, // 13 "Invoking 'use*()' method outside of invocation context.", // 14 - 'Cant access renderCtx for existing context', // 15 - 'Cant access document for existing context', // 16 - 'props are immutable', // 17 - '
component can only be used at the root of a Qwik component$()', // 18 - 'Props are immutable by default.', // 19 + '', // 15 unused + '', // 16 unused + '', // 17 unused + '', // 18 unused + '', // 19 unused `Calling a 'use*()' method outside 'component$(() => { HERE })' is not allowed. 'use*()' methods provide hooks to the 'component$' state and lifecycle, ie 'use' hooks can only be called synchronously within the 'component$' function or another 'use' method.\nSee https://qwik.dev/docs/components/tasks/#use-method-rules`, // 20 - 'Container is already paused. Skipping', // 21 - '', // 22 -- unused - 'When rendering directly on top of Document, the root node must be a ', // 23 - 'A node must have 2 children. The first one and the second one a ', // 24 - 'Invalid JSXNode type "{{0}}". It must be either a function or a string. Found:', // 25 - 'Tracking value changes can only be done to useStore() objects and component props', // 26 - 'Missing Object ID for captured object', // 27 + '', // 21 unused + '', // 22 unused + '', // 23 unused + '', // 24 unused + '', // 25 unused + '', // 26 unused + '', // 27 unused 'The provided Context reference "{{0}}" is not a valid context created by createContextId()', // 28 - ' is the root container, it can not be rendered inside a component', // 29 + 'SsrError(tag): {{0}}', // 29 'QRLs can not be resolved because it does not have an attached container. This means that the QRL does not know where it belongs inside the DOM, so it cant dynamically import() from a relative path.', // 30 'QRLs can not be dynamically resolved, because it does not have a chunk path', // 31 'The JSX ref attribute must be a Signal', // 32 + 'Serialization Error: Deserialization of data type {{0}} is not implemented', // 33 + 'Serialization Error: Expected vnode for ref prop, but got {{0}}', // 34 + 'Serialization Error: Cannot allocate data type {{0}}', // 35 + 'Serialization Error: Missing root id for {{0}}', // 36 + 'Serialization Error: Serialization of data type {{0}} is not implemented', // 37 + 'Serialization Error: Unvisited {{0}}', // 38 + 'Serialization Error: Missing QRL chunk for {{0}}', // 39 + 'The value of the textarea must be a string', // 40 + 'Unable to find q:container', // 41 + "Element must have 'q:container' attribute.", // 42 + 'Unknown vnode type {{0}}.', // 43 + 'Materialize error: missing element: {{0}} {{1}} {{2}}', // 44 + 'SsrError: {{0}}', // 45 + 'Cannot coerce a Signal, use `.value` instead', // 46 + 'useComputedSignal$ QRL {{0}} {{1}} returned a Promise', // 47 + 'ComputedSignal is read-only', // 48 + 'WrappedSignal is read-only', // 49 + 'SsrError: Promises not expected here.', // 50 + 'Attribute value is unsafe for SSR', // 51 ]; let text = MAP[code] ?? ''; if (parts.length) { @@ -49,47 +68,69 @@ export const codeToText = (code: number, ...parts: any[]): string => { return v; }); } - return `Code(${code}): ${text}`; + return `Code(Q${code}): ${text}`; } else { // cute little hack to give roughly the correct line number. Update the line number if it shifts. - return `Code(${code}) https://github.com/QwikDev/qwik/blob/main/packages/qwik/src/core/error/error.ts#L${8 + code}`; + return `Code(Q${code}) https://github.com/QwikDev/qwik/blob/main/packages/qwik/src/core/error/error.ts#L${8 + code}`; } }; -export const QError_stringifyClassOrStyle = 0; -export const QError_cannotSerializeNode = 1; -export const QError_runtimeQrlNoElement = 2; -export const QError_verifySerializable = 3; -export const QError_errorWhileRendering = 4; -export const QError_cannotRenderOverExistingContainer = 5; -export const QError_setProperty = 6; -export const QError_qrlOrError = 7; -export const QError_onlyObjectWrapped = 8; -export const QError_onlyLiteralWrapped = 9; -export const QError_qrlIsNotFunction = 10; -export const QError_dynamicImportFailed = 11; -export const QError_unknownTypeArgument = 12; -export const QError_notFoundContext = 13; -export const QError_useMethodOutsideContext = 14; -export const QError_missingRenderCtx = 15; -export const QError_missingDoc = 16; -export const QError_immutableProps = 17; -export const QError_hostCanOnlyBeAtRoot = 18; -export const QError_immutableJsxProps = 19; -export const QError_useInvokeContext = 20; -export const QError_containerAlreadyPaused = 21; -export const QError_unused_please_reuse = 22; -export const QError_rootNodeMustBeHTML = 23; -export const QError_strictHTMLChildren = 24; -export const QError_invalidJsxNodeType = 25; -export const QError_trackUseStore = 26; -export const QError_missingObjectId = 27; -export const QError_invalidContext = 28; -export const QError_canNotRenderHTML = 29; -export const QError_qrlMissingContainer = 30; -export const QError_qrlMissingChunk = 31; -export const QError_invalidRefValue = 32; -export const qError = (code: number, ...parts: any[]): Error => { - const text = codeToText(code, ...parts); - return logErrorAndStop(text, ...parts); +export const enum QError { + stringifyClassOrStyle = 0, + UNUSED_1 = 1, + UNUSED_2 = 2, + verifySerializable = 3, + UNUSED_4 = 4, + cannotRenderOverExistingContainer = 5, + UNUSED_6 = 6, + UNUSED_7 = 7, + UNUSED_8 = 8, + UNUSED_9 = 9, + qrlIsNotFunction = 10, + dynamicImportFailed = 11, + unknownTypeArgument = 12, + notFoundContext = 13, + useMethodOutsideContext = 14, + UNUSED_15 = 15, + UNUSED_16 = 16, + UNUSED_17 = 17, + UNUSED_18 = 18, + UNUSED_19 = 19, + useInvokeContext = 20, + UNUSED_21 = 21, + UNUSED_22 = 22, + UNUSED_23 = 23, + UNUSED_24 = 24, + UNUSED_25 = 25, + UNUSED_26 = 26, + UNUSED_27 = 27, + invalidContext = 28, + tagError = 29, + qrlMissingContainer = 30, + qrlMissingChunk = 31, + invalidRefValue = 32, + serializeErrorNotImplemented = 33, + serializeErrorExpectedVNode = 34, + serializeErrorCannotAllocate = 35, + serializeErrorMissingRootId = 36, + serializeErrorUnknownType = 37, + serializeErrorUnvisited = 38, + serializeErrorMissingChunk = 39, + wrongTextareaValue = 40, + containerNotFound = 41, + elementWithoutContainer = 42, + invalidVNodeType = 43, + materializeVNodeDataError = 44, + serverHostMismatch = 45, + cannotCoerceSignal = 46, + computedNotSync = 47, + computedReadOnly = 48, + wrappedReadOnly = 49, + promisesNotExpected = 50, + unsafeAttr = 51, +} + +export const qError = (code: number, errorMessageArgs: any[] = []): Error => { + const text = codeToText(code, ...errorMessageArgs); + return logErrorAndStop(text, ...errorMessageArgs); }; diff --git a/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts b/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts index a21dd50bb50..ba0b9ec8231 100644 --- a/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts +++ b/packages/qwik/src/core/shared/jsx/types/jsx-generated.ts @@ -596,7 +596,7 @@ type SpecialAttrs = { * For type: HTMLInputTypeAttribute, excluding 'button' | 'reset' | 'submit' | 'checkbox' | * 'radio' */ - 'bind:value'?: Signal; + 'bind:value'?: Signal; enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined; height?: Size | undefined; max?: number | string | undefined; diff --git a/packages/qwik/src/core/shared/platform/platform.ts b/packages/qwik/src/core/shared/platform/platform.ts index afa9730b3ea..2ec9c01dd97 100644 --- a/packages/qwik/src/core/shared/platform/platform.ts +++ b/packages/qwik/src/core/shared/platform/platform.ts @@ -1,5 +1,6 @@ +// keep this import from core/build so the cjs build works import { isServer } from '@qwik.dev/core/build'; -import { qError, QError_qrlMissingChunk, QError_qrlMissingContainer } from '../error/error'; +import { QError, qError } from '../error/error'; import { getSymbolHash } from '../qrl/qrl-class'; import { qDynamicPlatform } from '../utils/qdev'; import type { CorePlatform } from './types'; @@ -16,10 +17,10 @@ export const createPlatform = (): CorePlatform => { } } if (!url) { - throw qError(QError_qrlMissingChunk, symbolName); + throw qError(QError.qrlMissingChunk, [symbolName]); } if (!containerEl) { - throw qError(QError_qrlMissingContainer, url, symbolName); + throw qError(QError.qrlMissingContainer, [url, symbolName]); } const urlDoc = toUrl(containerEl.ownerDocument, containerEl, url).toString(); const urlCopy = new URL(urlDoc); diff --git a/packages/qwik/src/core/shared/prefetch-service-worker/prefetch.ts b/packages/qwik/src/core/shared/prefetch-service-worker/prefetch.ts index 5713d4aec8d..71decebce3d 100644 --- a/packages/qwik/src/core/shared/prefetch-service-worker/prefetch.ts +++ b/packages/qwik/src/core/shared/prefetch-service-worker/prefetch.ts @@ -1,3 +1,4 @@ +// keep this import from core/build so the cjs build works import { isDev } from '@qwik.dev/core/build'; import { _jsxSorted } from '../../internal'; import { useServerData } from '../../use/use-env-data'; diff --git a/packages/qwik/src/core/shared/qrl/qrl-class.ts b/packages/qwik/src/core/shared/qrl/qrl-class.ts index 2d5f63484f6..3deda85ec9b 100644 --- a/packages/qwik/src/core/shared/qrl/qrl-class.ts +++ b/packages/qwik/src/core/shared/qrl/qrl-class.ts @@ -1,6 +1,6 @@ import { isDev } from '@qwik.dev/core/build'; import { assertDefined } from '../error/assert'; -import { qError, QError_qrlIsNotFunction } from '../error/error'; +import { QError, qError } from '../error/error'; import { getPlatform, isServerPlatform } from '../platform/platform'; import { verifySerializable } from '../utils/serialize-utils'; import { @@ -109,7 +109,7 @@ export const createQRL = ( return (...args: QrlArgs): QrlReturn => maybeThen(resolveLazy(), (fn) => { if (!isFunction(fn)) { - throw qError(QError_qrlIsNotFunction); + throw qError(QError.qrlIsNotFunction); } if (beforeFn && beforeFn() === false) { return; diff --git a/packages/qwik/src/core/shared/qrl/qrl.ts b/packages/qwik/src/core/shared/qrl/qrl.ts index 41533c784fc..20a734262ad 100644 --- a/packages/qwik/src/core/shared/qrl/qrl.ts +++ b/packages/qwik/src/core/shared/qrl/qrl.ts @@ -1,4 +1,4 @@ -import { QError_dynamicImportFailed, QError_unknownTypeArgument, qError } from '../error/error'; +import { QError, qError } from '../error/error'; import { EMPTY_ARRAY } from '../utils/flyweight'; import { qSerialize } from '../utils/qdev'; import { isFunction, isString } from '../utils/types'; @@ -66,13 +66,13 @@ export const qrl = ( chunk = match[1]; } } else { - throw qError(QError_dynamicImportFailed, srcCode); + throw qError(QError.dynamicImportFailed, [srcCode]); } } } else if (isString(chunkOrFn)) { chunk = chunkOrFn; } else { - throw qError(QError_unknownTypeArgument, chunkOrFn); + throw qError(QError.unknownTypeArgument, [chunkOrFn]); } if (!announcedQRL.has(symbol)) { diff --git a/packages/qwik/src/core/shared/scheduler.ts b/packages/qwik/src/core/shared/scheduler.ts index 3bfa1df6464..aec222b077a 100644 --- a/packages/qwik/src/core/shared/scheduler.ts +++ b/packages/qwik/src/core/shared/scheduler.ts @@ -85,14 +85,15 @@ import type { QRLInternal } from './qrl/qrl-class'; import type { JSXOutput } from './jsx/types/jsx-node'; import { Task, TaskFlags, cleanupTask, runTask, type TaskFn } from '../use/use-task'; import { runResource, type ResourceDescriptor } from '../use/use-resource'; -import { logWarn, throwErrorAndStop } from './utils/log'; -import { isPromise, maybeThen, maybeThenPassError, safeCall } from './utils/promises'; +import { logWarn } from './utils/log'; +import { isPromise, maybeThenPassError, retryOnPromise, safeCall } from './utils/promises'; import type { ValueOrPromise } from './utils/types'; import { isDomContainer } from '../client/dom-container'; import { ElementVNodeProps, VNodeFlags, VNodeProps, + type ClientContainer, type ElementVNode, type VirtualVNode, } from '../client/types'; @@ -110,36 +111,41 @@ import { type DomContainer } from '../client/dom-container'; import { serializeAttribute } from './utils/styles'; import type { OnRenderFn } from './component.public'; import type { Props } from './jsx/jsx-runtime'; +import { QScopedStyle } from './utils/markers'; +import { addComponentStylePrefix } from './utils/scoped-styles'; +import { type WrappedSignal, type ComputedSignal, triggerEffects } from '../signal/signal'; +import type { TargetType } from '../signal/store'; +import { QError, qError } from './error/error'; // Turn this on to get debug output of what the scheduler is doing. const DEBUG: boolean = false; export const enum ChoreType { /// MASKS defining three levels of sorting - MACRO /* ***************** */ = 0b111_0000, + MACRO /* **************************** */ = 0b1111_0000, /* order of elements (not encoded here) */ - MICRO /* ***************** */ = 0b000_1111, + MICRO /* **************************** */ = 0b0000_1111, /** Ensure tha the QRL promise is resolved before processing next chores in the queue */ - QRL_RESOLVE /* *********** */ = 0b000_0001, - RESOURCE /* ************** */ = 0b000_0010, - TASK /* ****************** */ = 0b000_0011, - NODE_DIFF /* ************* */ = 0b000_0100, - NODE_PROP /* ************* */ = 0b000_0101, - COMPONENT_SSR /* ********* */ = 0b000_0110, - COMPONENT /* ************* */ = 0b000_0111, - WAIT_FOR_COMPONENTS /* *** */ = 0b001_0000, - JOURNAL_FLUSH /* ********* */ = 0b011_0000, - VISIBLE /* *************** */ = 0b100_0000, - CLEANUP_VISIBLE /* ******* */ = 0b101_0000, - WAIT_FOR_ALL /* ********** */ = 0b111_1111, + QRL_RESOLVE /* ********************** */ = 0b0000_0001, + RESOURCE /* ************************* */ = 0b0000_0010, + TASK /* ***************************** */ = 0b0000_0011, + NODE_DIFF /* ************************ */ = 0b0000_0100, + NODE_PROP /* ************************ */ = 0b0000_0101, + COMPONENT_SSR /* ******************** */ = 0b0000_0110, + COMPONENT /* ************************ */ = 0b0000_0111, + RECOMPUTE_AND_SCHEDULE_EFFECTS /* *** */ = 0b0000_1000, + JOURNAL_FLUSH /* ******************** */ = 0b0001_0000, + VISIBLE /* ************************** */ = 0b0010_0000, + CLEANUP_VISIBLE /* ****************** */ = 0b0011_0000, + WAIT_FOR_ALL /* ********************* */ = 0b1111_1111, } export interface Chore { $type$: ChoreType; $idx$: number | string; $host$: HostElement; - $target$: HostElement | QRLInternal<(...args: unknown[]) => unknown> | null; + $target$: ChoreTarget | null; $payload$: unknown; $resolve$: (value: any) => void; $promise$: Promise; @@ -158,6 +164,8 @@ export interface NodePropPayload extends NodePropData { export type Scheduler = ReturnType; +type ChoreTarget = HostElement | QRLInternal<(...args: unknown[]) => unknown> | Signal | TargetType; + export const createScheduler = ( container: Container, scheduleDrain: () => void, @@ -180,7 +188,6 @@ export const createScheduler = ( ): ValueOrPromise; function schedule(type: ChoreType.JOURNAL_FLUSH): ValueOrPromise; function schedule(type: ChoreType.WAIT_FOR_ALL): ValueOrPromise; - function schedule(type: ChoreType.WAIT_FOR_COMPONENTS): ValueOrPromise; /** * Schedule rendering of a component. * @@ -190,6 +197,11 @@ export const createScheduler = ( * @param props- Props to pass to the component. * @param waitForChore? = false */ + function schedule( + type: ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, + host: HostElement | null, + target: Signal + ): ValueOrPromise; function schedule( type: ChoreType.TASK | ChoreType.VISIBLE | ChoreType.RESOURCE, task: Task @@ -223,13 +235,10 @@ export const createScheduler = ( function schedule( type: ChoreType, hostOrTask: HostElement | Task | null = null, - targetOrQrl: HostElement | QRLInternal<(...args: unknown[]) => unknown> | string | null = null, + targetOrQrl: ChoreTarget | string | null = null, payload: any = null ): ValueOrPromise { - const runLater: boolean = - type !== ChoreType.WAIT_FOR_ALL && - type !== ChoreType.WAIT_FOR_COMPONENTS && - type !== ChoreType.COMPONENT_SSR; + const runLater: boolean = type !== ChoreType.WAIT_FOR_ALL && type !== ChoreType.COMPONENT_SSR; const isTask = type === ChoreType.TASK || type === ChoreType.VISIBLE || @@ -246,7 +255,7 @@ export const createScheduler = ( ? targetOrQrl : 0, $host$: isTask ? (hostOrTask as Task).$el$ : (hostOrTask as HostElement), - $target$: targetOrQrl as QRLInternal<(...args: unknown[]) => unknown>, + $target$: targetOrQrl as ChoreTarget | null, $payload$: isTask ? hostOrTask : payload, $resolve$: null!, $promise$: null!, @@ -334,9 +343,17 @@ export const createScheduler = ( chore.$payload$ as Props | null ), (jsx) => { - return chore.$type$ === ChoreType.COMPONENT - ? maybeThen(container.processJsx(host, jsx), () => jsx) - : jsx; + if (chore.$type$ === ChoreType.COMPONENT) { + const styleScopedId = container.getHostProp(host, QScopedStyle); + return vnode_diff( + container as ClientContainer, + jsx, + host as VirtualVNode, + addComponentStylePrefix(styleScopedId) + ); + } else { + return jsx; + } }, (err: any) => container.handleError(err, host) ); @@ -390,6 +407,20 @@ export const createScheduler = ( returnValue = !target.resolved ? target.resolve() : null; break; } + case ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS: { + const target = chore.$target$ as ComputedSignal | WrappedSignal; + const forceRunEffects = target.$forceRunEffects$; + target.$forceRunEffects$ = false; + if (!target.$effects$?.length) { + break; + } + returnValue = retryOnPromise(() => { + if (target.$computeIfNeeded$() || forceRunEffects) { + triggerEffects(container, target, target.$effects$); + } + }); + break; + } } return maybeThenPassError(returnValue, (value) => { DEBUG && debugTrace('execute.DONE', null, currentChore, choreQueue); @@ -466,7 +497,7 @@ function choreComparator(a: Chore, b: Chore, shouldThrowOnHostMismatch: boolean) This can lead to inconsistencies between Server-Side Rendering (SSR) and Client-Side Rendering (CSR). Problematic Node: ${aHost.toString()}`; if (shouldThrowOnHostMismatch) { - throwErrorAndStop(errorMessage); + throw qError(QError.serverHostMismatch, [errorMessage]); } logWarn(errorMessage); return null; @@ -487,7 +518,9 @@ function choreComparator(a: Chore, b: Chore, shouldThrowOnHostMismatch: boolean) if ( a.$target$ !== b.$target$ && ((a.$type$ === ChoreType.QRL_RESOLVE && b.$type$ === ChoreType.QRL_RESOLVE) || - (a.$type$ === ChoreType.NODE_PROP && b.$type$ === ChoreType.NODE_PROP)) + (a.$type$ === ChoreType.NODE_PROP && b.$type$ === ChoreType.NODE_PROP) || + (a.$type$ === ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS && + b.$type$ === ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS)) ) { // 1 means that we are going to process chores as FIFO return 1; @@ -543,11 +576,11 @@ function debugChoreToString(chore: Chore): string { [ChoreType.NODE_PROP]: 'NODE_PROP', [ChoreType.COMPONENT]: 'COMPONENT', [ChoreType.COMPONENT_SSR]: 'COMPONENT_SSR', + [ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS]: 'RECOMPUTE_SIGNAL', [ChoreType.JOURNAL_FLUSH]: 'JOURNAL_FLUSH', [ChoreType.VISIBLE]: 'VISIBLE', [ChoreType.CLEANUP_VISIBLE]: 'CLEANUP_VISIBLE', [ChoreType.WAIT_FOR_ALL]: 'WAIT_FOR_ALL', - [ChoreType.WAIT_FOR_COMPONENTS]: 'WAIT_FOR_COMPONENTS', } as any )[chore.$type$] || 'UNKNOWN: ' + chore.$type$; const host = String(chore.$host$).replaceAll(/\n.*/gim, ''); diff --git a/packages/qwik/src/core/shared/scheduler.unit.tsx b/packages/qwik/src/core/shared/scheduler.unit.tsx index 55e08f87ff3..fec2e5a0bbc 100644 --- a/packages/qwik/src/core/shared/scheduler.unit.tsx +++ b/packages/qwik/src/core/shared/scheduler.unit.tsx @@ -36,7 +36,6 @@ describe('scheduler', () => { document = createDocument(); document.body.setAttribute(QContainerAttr, 'paused'); const container = getDomContainer(document.body); - container.processJsx = () => null!; scheduler = createScheduler( container, () => null, diff --git a/packages/qwik/src/core/shared/shared-container.ts b/packages/qwik/src/core/shared/shared-container.ts index 94bcefee2c0..bf0577c2763 100644 --- a/packages/qwik/src/core/shared/shared-container.ts +++ b/packages/qwik/src/core/shared/shared-container.ts @@ -1,9 +1,7 @@ -import type { JSXOutput } from './jsx/types/jsx-node'; import type { ContextId } from '../use/use-context'; -import { trackSignal } from '../use/use-core'; -import type { ValueOrPromise } from './utils/types'; +import { trackSignalAndAssignHost } from '../use/use-core'; import { version } from '../version'; -import type { Effect, EffectData } from '../signal/signal'; +import type { EffectPropData } from '../signal/signal'; import type { Signal } from '../signal/signal.public'; import type { ISsrNode, StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; import type { Scheduler } from './scheduler'; @@ -41,8 +39,13 @@ export abstract class _SharedContainer implements Container { this.$scheduler$ = createScheduler(this, scheduleDrain, journalFlush); } - trackSignalValue(signal: Signal, subscriber: Effect, property: string, data: EffectData): T { - return trackSignal(() => signal.value, subscriber, property, this, data); + trackSignalValue( + signal: Signal, + subscriber: HostElement, + property: string, + data: EffectPropData + ): T { + return trackSignalAndAssignHost(signal, subscriber, property, this, data); } serializationCtxFactory( @@ -69,7 +72,6 @@ export abstract class _SharedContainer implements Container { } abstract ensureProjectionResolved(host: HostElement): void; - abstract processJsx(host: HostElement, jsx: JSXOutput): ValueOrPromise; abstract handleError(err: any, $host$: HostElement): void; abstract getParentHost(host: HostElement): HostElement | null; abstract setContext(host: HostElement, context: ContextId, value: T): void; diff --git a/packages/qwik/src/core/shared/shared-serialization.ts b/packages/qwik/src/core/shared/shared-serialization.ts index bd2d98574ca..0867061eba5 100644 --- a/packages/qwik/src/core/shared/shared-serialization.ts +++ b/packages/qwik/src/core/shared/shared-serialization.ts @@ -2,12 +2,13 @@ import { isDev } from '../../build/index.dev'; import type { StreamWriter } from '../../server/types'; -import { VNodeDataFlag, type VNodeData } from '../../server/vnode-data'; +import { VNodeDataFlag } from '../../server/types'; +import type { VNodeData } from '../../server/vnode-data'; import { type DomContainer } from '../client/dom-container'; import type { VNode } from '../client/types'; import { vnode_getNode, vnode_isVNode, vnode_locate, vnode_toString } from '../client/vnode'; import { NEEDS_COMPUTATION } from '../signal/flags'; -import { ComputedSignal, EffectData, Signal, WrappedSignal } from '../signal/signal'; +import { ComputedSignal, EffectPropData, Signal, WrappedSignal } from '../signal/signal'; import type { Subscriber } from '../signal/signal-subscriber'; import { STORE_ARRAY_PROP, @@ -16,12 +17,13 @@ import { getStoreTarget, isStore, } from '../signal/store'; -import type { ISsrNode, SymbolToChunkResolver } from '../ssr/ssr-types'; +import type { SsrAttrs, ISsrNode, SymbolToChunkResolver } from '../ssr/ssr-types'; import { untrack } from '../use/use-core'; import { createResourceReturn, type ResourceReturnInternal } from '../use/use-resource'; import { Task, isTask } from '../use/use-task'; import { SERIALIZABLE_STATE, componentQrl, isQwikComponent } from './component.public'; import { assertDefined, assertTrue } from './error/assert'; +import { QError, qError } from './error/error'; import { Fragment, JSXNodeImpl, @@ -39,13 +41,12 @@ import { type SyncQRLInternal, } from './qrl/qrl-class'; import type { QRL } from './qrl/qrl.public'; -import { ChoreType } from './scheduler'; +import { ChoreType, type NodePropData } from './scheduler'; import type { DeserializeContainer, HostElement, ObjToProxyMap } from './types'; import { _CONST_PROPS, _VAR_PROPS } from './utils/constants'; import { isElement, isNode } from './utils/element'; import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight'; -import { throwErrorAndStop } from './utils/log'; -import { ELEMENT_ID } from './utils/markers'; +import { ELEMENT_ID, ELEMENT_KEY } from './utils/markers'; import { isPromise } from './utils/promises'; import { fastSkipSerialize } from './utils/serialize-utils'; import { type ValueOrPromise } from './utils/types'; @@ -194,7 +195,10 @@ const inflate = (container: DeserializeContainer, target: any, typeId: TypeIds, if (valType === TypeIds.RootRef || valType >= TypeIds.Error) { Object.defineProperty(target, key, { get() { - return deserializeData(container, valType, valData); + const value = deserializeData(container, valType, valData); + // after first deserialize, we can replace the Object.defineProperty with the value + target[key] = value; + return value; }, set(value: unknown) { Object.defineProperty(target, key, { @@ -267,12 +271,13 @@ const inflate = (container: DeserializeContainer, target: any, typeId: TypeIds, } case TypeIds.WrappedSignal: { const signal = target as WrappedSignal; - const d = data as [number, unknown[], Subscriber[], unknown, ...any[]]; + const d = data as [number, unknown[], Subscriber[], unknown, HostElement, ...any[]]; signal.$func$ = container.getSyncFn(d[0]); signal.$args$ = d[1]; signal.$effectDependencies$ = d[2]; signal.$untrackedValue$ = d[3]; - signal.$effects$ = d.slice(4); + signal.$hostElement$ = d[4]; + signal.$effects$ = d.slice(5); break; } case TypeIds.ComputedSignal: { @@ -374,12 +379,13 @@ const inflate = (container: DeserializeContainer, target: any, typeId: TypeIds, propsProxy[_CONST_PROPS] = (data as any)[1]; break; case TypeIds.EffectData: { - const effectData = target as EffectData; - effectData.data = (data as any[])[0]; + const effectData = target as EffectPropData; + effectData.data.$scopedStyleIdPrefix$ = (data as any[])[0]; + effectData.data.$isConst$ = (data as any[])[1]; break; } default: - return throwErrorAndStop('Not implemented'); + throw qError(QError.serializeErrorNotImplemented, [typeId]); } }; @@ -513,13 +519,13 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow if (vnode_isVNode(vNode)) { return vnode_getNode(vNode); } else { - return throwErrorAndStop('expected vnode for ref prop, but got ' + typeof vNode); + throw qError(QError.serializeErrorExpectedVNode, [typeof vNode]); } case TypeIds.EffectData: - return new EffectData(null!); + return new EffectPropData({} as NodePropData); default: - return throwErrorAndStop('unknown allocate type: ' + typeId); + throw qError(QError.serializeErrorCannotAllocate, [typeId]); } }; @@ -713,7 +719,7 @@ export const createSerializationContext = ( $getRootId$: (obj: any) => { const id = map.get(obj); if (!id || id === -1) { - return throwErrorAndStop('Missing root id for: ', obj); + throw qError(QError.serializeErrorMissingRootId, [obj]); } return id; }, @@ -834,13 +840,22 @@ export const createSerializationContext = ( if (obj.$args$) { discoveredValues.push(...obj.$args$); } + if (obj.$hostElement$) { + discoveredValues.push(obj.$hostElement$); + } } else if (obj instanceof ComputedSignal) { discoveredValues.push(obj.$computeQrl$); } } else if (obj instanceof Task) { discoveredValues.push(obj.$el$, obj.$qrl$, obj.$state$, obj.$effectDependencies$); } else if (isSsrNode(obj)) { - discoveredValues.push(obj.vnodeData); + discoverValuesForVNodeData(obj.vnodeData, discoveredValues); + + if (obj.childrenVNodeData && obj.childrenVNodeData.length) { + for (const data of obj.childrenVNodeData) { + discoverValuesForVNodeData(data, discoveredValues); + } + } } else if (isDomRef!(obj)) { discoveredValues.push(obj.$ssrNode$.id); } else if (isJSXNode(obj)) { @@ -863,14 +878,14 @@ export const createSerializationContext = ( } ); promises.push(obj); - } else if (obj instanceof EffectData) { + } else if (obj instanceof EffectPropData) { discoveredValues.push(obj.data); } else if (isObjectLiteral(obj)) { Object.entries(obj).forEach(([key, value]) => { discoveredValues.push(key, value); }); } else { - return throwErrorAndStop('Unknown type: ' + obj); + throw qError(QError.serializeErrorUnknownType, [obj]); } }; @@ -903,6 +918,23 @@ export const createSerializationContext = ( } }; +const isSsrAttrs = (value: number | SsrAttrs): value is SsrAttrs => + Array.isArray(value) && value.length > 0; + +const discoverValuesForVNodeData = (vnodeData: VNodeData, discoveredValues: unknown[]) => { + for (const value of vnodeData) { + if (isSsrAttrs(value)) { + for (let i = 1; i < value.length; i += 2) { + if (value[i - 1] === ELEMENT_KEY) { + continue; + } + const attrValue = value[i]; + discoveredValues.push(attrValue); + } + } + } +}; + const promiseResults = new WeakMap, [boolean, unknown]>(); /** @@ -1029,7 +1061,7 @@ function serialize(serializationContext: SerializationContext): void { } else if (value === NEEDS_COMPUTATION) { output(TypeIds.Constant, Constants.NEEDS_COMPUTATION); } else { - throwErrorAndStop('Unknown type: ' + typeof value); + throw qError(QError.serializeErrorUnknownType, [typeof value]); } }; @@ -1064,15 +1096,15 @@ function serialize(serializationContext: SerializationContext): void { ? [varProps] : 0; output(TypeIds.PropsProxy, out); - } else if (value instanceof EffectData) { - output(TypeIds.EffectData, [value.data]); + } else if (value instanceof EffectPropData) { + output(TypeIds.EffectData, [value.data.$scopedStyleIdPrefix$, value.data.$isConst$]); } else if (isStore(value)) { if (isResource(value)) { // let render know about the resource serializationContext.$resources$.add(value); const res = promiseResults.get(value.value); if (!res) { - return throwErrorAndStop('Unvisited Resource'); + throw qError(QError.serializeErrorUnvisited, ['resource']); } output(TypeIds.Resource, [...res, getStoreHandler(value)!.$effects$]); } else { @@ -1133,6 +1165,7 @@ function serialize(serializationContext: SerializationContext): void { ...serializeWrappingFn(serializationContext, value), value.$effectDependencies$, v, + value.$hostElement$, ...(value.$effects$ || []), ]); } else if (value instanceof ComputedSignal) { @@ -1232,7 +1265,7 @@ function serialize(serializationContext: SerializationContext): void { } else if (isPromise(value)) { const res = promiseResults.get(value); if (!res) { - return throwErrorAndStop('Unvisited Promise'); + throw qError(QError.serializeErrorUnvisited, ['promise']); } output(TypeIds.Promise, res); } else if (value instanceof Uint8Array) { @@ -1243,7 +1276,7 @@ function serialize(serializationContext: SerializationContext): void { const out = btoa(buf).replace(/=+$/, ''); output(TypeIds.Uint8Array, out); } else { - return throwErrorAndStop('implement'); + throw qError(QError.serializeErrorUnknownType, [typeof value]); } }; @@ -1304,7 +1337,7 @@ export function qrlToString( } } if (!chunk) { - throwErrorAndStop('Missing chunk for: ' + value.$symbol$); + throw qError(QError.qrlMissingChunk, [value.$symbol$]); } if (chunk.startsWith('./')) { chunk = chunk.slice(2); diff --git a/packages/qwik/src/core/shared/shared-serialization.unit.ts b/packages/qwik/src/core/shared/shared-serialization.unit.ts index b4bf374ece8..22ccbff3efb 100644 --- a/packages/qwik/src/core/shared/shared-serialization.unit.ts +++ b/packages/qwik/src/core/shared/shared-serialization.unit.ts @@ -1,7 +1,7 @@ import { $, component$ } from '@qwik.dev/core'; import { describe, expect, it } from 'vitest'; import { _fnSignal, _wrapProp } from '../internal'; -import { EffectData, type Signal } from '../signal/signal'; +import { EffectPropData, type Signal } from '../signal/signal'; import { createComputed$, createSignal, isSignal } from '../signal/signal.public'; import { StoreFlags, createStore } from '../signal/store'; import { createResourceReturn } from '../use/use-resource'; @@ -364,6 +364,7 @@ describe('shared-serialization', () => { ] Constant null Number 4 + Constant null ] 1 WrappedSignal [ Number 1 @@ -372,8 +373,9 @@ describe('shared-serialization', () => { ] Constant null Constant undefined + Constant null ] - (53 chars)" + (61 chars)" `); }); it(title(TypeIds.ComputedSignal), async () => { @@ -423,15 +425,14 @@ describe('shared-serialization', () => { it.todo(title(TypeIds.JSXNode)); it.todo(title(TypeIds.PropsProxy)); it(title(TypeIds.EffectData), async () => { - expect(await dump(new EffectData({ hi: true }))).toMatchInlineSnapshot(` + expect(await dump(new EffectPropData({ $isConst$: true, $scopedStyleIdPrefix$: null }))) + .toMatchInlineSnapshot(` " 0 EffectData [ - Object [ - String "hi" - Constant true - ] + Constant null + Constant true ] - (22 chars)" + (14 chars)" `); }); }); @@ -618,10 +619,12 @@ describe('shared-serialization', () => { it.todo(title(TypeIds.JSXNode)); it.todo(title(TypeIds.PropsProxy)); it(title(TypeIds.EffectData), async () => { - const objs = await serialize(new EffectData({ hi: true })); - const effect = deserialize(objs)[0] as EffectData; - expect(effect).toBeInstanceOf(EffectData); - expect(effect.data).toEqual({ hi: true }); + const objs = await serialize( + new EffectPropData({ $isConst$: true, $scopedStyleIdPrefix$: null }) + ); + const effect = deserialize(objs)[0] as EffectPropData; + expect(effect).toBeInstanceOf(EffectPropData); + expect(effect.data).toEqual({ $isConst$: true, $scopedStyleIdPrefix$: null }); }); }); diff --git a/packages/qwik/src/core/shared/types.ts b/packages/qwik/src/core/shared/types.ts index 74279d62cf4..fa27a699be0 100644 --- a/packages/qwik/src/core/shared/types.ts +++ b/packages/qwik/src/core/shared/types.ts @@ -1,6 +1,4 @@ -import type { JSXOutput } from './jsx/types/jsx-node'; import type { ContextId } from '../use/use-context'; -import type { ValueOrPromise } from './utils/types'; import type { VNode } from '../client/types'; import type { ISsrNode, StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; import type { Scheduler } from './scheduler'; @@ -25,8 +23,6 @@ export interface Container { readonly $serverData$: Record; $currentUniqueId$: number; - // TODO(misko): I think `processJsx` can be deleted. - processJsx(host: HostElement, jsx: JSXOutput): ValueOrPromise; handleError(err: any, $host$: HostElement): void; getParentHost(host: HostElement): HostElement | null; setContext(host: HostElement, context: ContextId, value: T): void; diff --git a/packages/qwik/src/core/shared/utils/log.ts b/packages/qwik/src/core/shared/utils/log.ts index 23d3241c784..a8ae2b07322 100644 --- a/packages/qwik/src/core/shared/utils/log.ts +++ b/packages/qwik/src/core/shared/utils/log.ts @@ -53,7 +53,7 @@ const createAndLogError = (asyncThrow: boolean, message?: any, ...optionalParams // display the error message first, then the optional params, and finally the stack trace // the stack needs to be displayed last because the given params will be lost among large stack traces so it will // provide a bad developer experience - console.error('%cQWIK ERROR', STYLE, err.message, ...optionalParams, err.stack); + !qTest && console.error('%cQWIK ERROR', STYLE, err.message, ...optionalParams, err.stack); asyncThrow && !qTest && diff --git a/packages/qwik/src/core/shared/utils/markers.ts b/packages/qwik/src/core/shared/utils/markers.ts index 1fdcdf520e1..d36f8d4975c 100644 --- a/packages/qwik/src/core/shared/utils/markers.ts +++ b/packages/qwik/src/core/shared/utils/markers.ts @@ -92,6 +92,7 @@ export const ELEMENT_SELF_ID = -1; export const ELEMENT_ID_SELECTOR = '[q\\:id]'; export const ELEMENT_ID_PREFIX = '#'; export const INLINE_FN_PREFIX = '@'; +export const Q_PREFIX = 'q:'; /** Non serializable markers - always begins with `:` character */ export const NON_SERIALIZABLE_MARKER_PREFIX = ':'; diff --git a/packages/qwik/src/core/shared/utils/promises.ts b/packages/qwik/src/core/shared/utils/promises.ts index 071ce34db2d..18597bb3530 100644 --- a/packages/qwik/src/core/shared/utils/promises.ts +++ b/packages/qwik/src/core/shared/utils/promises.ts @@ -46,7 +46,7 @@ export const maybeThenPassError = ( }; export const shouldNotError = (reason: any): any => { - throwErrorAndStop('QWIK ERROR:', reason); + throwErrorAndStop(reason); }; export const maybeThenMap = ( diff --git a/packages/qwik/src/core/shared/utils/serialize-utils.ts b/packages/qwik/src/core/shared/utils/serialize-utils.ts index 78de4e11dbd..1208bb816e9 100644 --- a/packages/qwik/src/core/shared/utils/serialize-utils.ts +++ b/packages/qwik/src/core/shared/utils/serialize-utils.ts @@ -1,6 +1,5 @@ -import { QError_verifySerializable, qError } from '../error/error'; +import { QError, qError } from '../error/error'; import { isNode } from './element'; -import { throwErrorAndStop } from './log'; import { isPromise } from './promises'; import { isArray, isFunction, isObject, isSerializableObject } from './types'; import { canSerialize } from '../shared-serialization'; @@ -43,7 +42,7 @@ const _verifySerializable = (value: T, seen: Set, ctx: string, preMessag // Make sure the array has no holes unwrapped.forEach((v, i) => { if (i !== expectIndex) { - throw qError(QError_verifySerializable, unwrapped); + throw qError(QError.verifySerializable, [unwrapped]); } _verifySerializable(v, seen, ctx + '[' + i + ']'); expectIndex = i + 1; @@ -79,8 +78,7 @@ const _verifySerializable = (value: T, seen: Set, ctx: string, preMessag value )});\n\nPlease check out https://qwik.dev/docs/advanced/qrl/ for more information.`; } - console.error('Trying to serialize', value); - throwErrorAndStop(message); + throw qError(QError.verifySerializable, [message]); } return value; }; diff --git a/packages/qwik/src/core/shared/utils/styles.ts b/packages/qwik/src/core/shared/utils/styles.ts index 3447b302bd5..1c682ca9647 100644 --- a/packages/qwik/src/core/shared/utils/styles.ts +++ b/packages/qwik/src/core/shared/utils/styles.ts @@ -1,5 +1,5 @@ import type { ClassList } from '../jsx/types/jsx-qwik-attributes'; -import { QError_stringifyClassOrStyle, qError } from '../error/error'; +import { QError, qError } from '../error/error'; import { isPreventDefault } from './event-names'; import { isClassAttr } from './scoped-styles'; import { isArray, isString } from './types'; @@ -48,7 +48,7 @@ export const stringifyStyle = (obj: any): string => { } if (typeof obj == 'object') { if (isArray(obj)) { - throw qError(QError_stringifyClassOrStyle, obj, 'style'); + throw qError(QError.stringifyClassOrStyle, [obj, 'style']); } else { const chunks: string[] = []; for (const key in obj) { @@ -70,7 +70,7 @@ export const stringifyStyle = (obj: any): string => { }; export const serializeBooleanOrNumberAttribute = (value: any) => { - return value != null ? String(value) : null; + return value != null ? (typeof value === 'number' ? value : String(value)) : null; }; export function serializeAttribute( diff --git a/packages/qwik/src/core/shared/utils/styles.unit.ts b/packages/qwik/src/core/shared/utils/styles.unit.ts index 71073711bc2..0172fc31ce5 100644 --- a/packages/qwik/src/core/shared/utils/styles.unit.ts +++ b/packages/qwik/src/core/shared/utils/styles.unit.ts @@ -169,7 +169,7 @@ suite('stringifyStyle', () => { test('should throw an error for array', () => { assert.throws( () => stringifyStyle([]), - 'Code(0): Error while serializing class or style attributes' + 'Code(Q0): Error while serializing class or style attributes' ); }); diff --git a/packages/qwik/src/core/shared/vnode-data-types.ts b/packages/qwik/src/core/shared/vnode-data-types.ts index f850b0ba94a..4dd368bb0d6 100644 --- a/packages/qwik/src/core/shared/vnode-data-types.ts +++ b/packages/qwik/src/core/shared/vnode-data-types.ts @@ -42,11 +42,15 @@ export const VNodeDataSeparator = { ADVANCE_8192: /* ****** */ 46, // `.` is vNodeData separator skipping 4096. }; -/** VNodeDataChar contains information about the VNodeData used for encoding props */ +/** + * VNodeDataChar contains information about the VNodeData used for encoding props. + * + * Available character ranges: 59 - 64, 91 - 94, 96, 123 - 126 + */ export const VNodeDataChar = { - OPEN: /* ************** */ 123, // `{` is the start of the VNodeData. + OPEN: /* ************** */ 123, // `{` is the start of the VNodeData for a virtual element. OPEN_CHAR: /* ****** */ '{', - CLOSE: /* ************* */ 125, // `}` is the end of the VNodeData. + CLOSE: /* ************* */ 125, // `}` is the end of the VNodeData for a virtual element. CLOSE_CHAR: /* ***** */ '}', SCOPED_STYLE: /* ******* */ 59, // `;` - `q:sstyle` - Style attribute. @@ -63,12 +67,14 @@ export const VNodeDataChar = { KEY_CHAR: /* ******** */ '@', SEQ: /* **************** */ 91, // `[` - `q:seq' - Seq value from `useSequentialScope()` SEQ_CHAR: /* ******** */ '[', - DON_T_USE: /* ********** */ 93, // `\` - SKIP because `\` is used as escaping + DON_T_USE: /* ********** */ 92, // `\` - SKIP because `\` is used as escaping DON_T_USE_CHAR: '\\', CONTEXT: /* ************ */ 93, // `]` - `q:ctx' - Component context/props CONTEXT_CHAR: /* **** */ ']', SEQ_IDX: /* ************ */ 94, // `^` - `q:seqIdx' - Sequential scope id SEQ_IDX_CHAR: /* **** */ '^', + SUBS: /* *************** */ 96, // '`' - `q:subs' - Effect dependencies/subscriptions + SUBS_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/signal/signal-subscriber.ts b/packages/qwik/src/core/signal/signal-subscriber.ts index 8857f7cc100..7682e88d237 100644 --- a/packages/qwik/src/core/signal/signal-subscriber.ts +++ b/packages/qwik/src/core/signal/signal-subscriber.ts @@ -1,7 +1,8 @@ import { QSubscribers } from '../shared/utils/markers'; import type { VNode } from '../client/types'; -import { vnode_getProp } from '../client/vnode'; +import { ensureMaterialized, vnode_getProp, vnode_isElementVNode } from '../client/vnode'; import { EffectSubscriptionsProp, WrappedSignal, isSignal } from './signal'; +import type { Container } from '../shared/types'; export abstract class Subscriber { $effectDependencies$: Subscriber[] | null = null; @@ -11,8 +12,11 @@ export function isSubscriber(value: unknown): value is Subscriber { return value instanceof Subscriber || value instanceof WrappedSignal; } -export function clearVNodeEffectDependencies(value: VNode): void { - const effects = vnode_getProp(value, QSubscribers, null); +export function clearVNodeEffectDependencies(container: Container, value: VNode): void { + if (vnode_isElementVNode(value)) { + ensureMaterialized(value); + } + const effects = vnode_getProp(value, QSubscribers, container.$getObjectById$); if (!effects) { return; } @@ -42,16 +46,29 @@ function clearEffects(subscriber: Subscriber, value: Subscriber | VNode): boolea return false; } const effectSubscriptions = (subscriber as WrappedSignal).$effects$; - if (!effectSubscriptions) { - return false; + const hostElement = (subscriber as WrappedSignal).$hostElement$; + + if (hostElement && hostElement === value) { + (subscriber as WrappedSignal).$hostElement$ = null; } let subscriptionRemoved = false; - for (let i = effectSubscriptions.length - 1; i >= 0; i--) { - const effect = effectSubscriptions[i]; - if (effect[EffectSubscriptionsProp.EFFECT] === value) { - effectSubscriptions.splice(i, 1); - subscriptionRemoved = true; + if (effectSubscriptions) { + for (let i = effectSubscriptions.length - 1; i >= 0; i--) { + const effect = effectSubscriptions[i]; + if (effect[EffectSubscriptionsProp.EFFECT] === value) { + effectSubscriptions.splice(i, 1); + subscriptionRemoved = true; + } } } + + // clear the effects of the arguments + const args = (subscriber as WrappedSignal).$args$; + if (args) { + for (let i = args.length - 1; i >= 0; i--) { + clearEffects(args[i], subscriber); + } + } + return subscriptionRemoved; } diff --git a/packages/qwik/src/core/signal/signal.ts b/packages/qwik/src/core/signal/signal.ts index be114e3b45a..38804e1dfe9 100644 --- a/packages/qwik/src/core/signal/signal.ts +++ b/packages/qwik/src/core/signal/signal.ts @@ -17,12 +17,11 @@ import { type QRLInternal } from '../shared/qrl/qrl-class'; import type { QRL } from '../shared/qrl/qrl.public'; import { trackSignal, tryGetInvokeContext } from '../use/use-core'; import { Task, TaskFlags, isTask } from '../use/use-task'; -import { logError, throwErrorAndStop } from '../shared/utils/log'; import { ELEMENT_PROPS, OnRenderProp, QSubscribers } from '../shared/utils/markers'; -import { isPromise, retryOnPromise } from '../shared/utils/promises'; +import { isPromise } from '../shared/utils/promises'; import { qDev } from '../shared/utils/qdev'; import type { VNode } from '../client/types'; -import { vnode_getProp, vnode_isVirtualVNode, vnode_isVNode, vnode_setProp } from '../client/vnode'; +import { vnode_getProp, vnode_isTextVNode, vnode_isVNode, vnode_setProp } from '../client/vnode'; import { ChoreType, type NodePropData, type NodePropPayload } from '../shared/scheduler'; import type { Container, HostElement } from '../shared/types'; import type { ISsrNode } from '../ssr/ssr-types'; @@ -32,6 +31,7 @@ import { isSubscriber, Subscriber } from './signal-subscriber'; import type { Props } from '../shared/jsx/jsx-runtime'; import type { OnRenderFn } from '../shared/component.public'; import { NEEDS_COMPUTATION } from './flags'; +import { QError, qError } from '../shared/error/error'; const DEBUG = false; @@ -76,10 +76,10 @@ export const isSignal = (value: any): value is ISignal => { export type Effect = Task | VNode | ISsrNode | Signal; /** @internal */ -export class EffectData = Record> { - data: T; +export class EffectPropData { + data: NodePropData; - constructor(data: T) { + constructor(data: NodePropData) { this.data = data; } } @@ -127,7 +127,7 @@ export type EffectSubscriptions = [ ], // List of signals to release ...( - | EffectData // Metadata for the effect + | EffectPropData // Metadata for the effect | string // List of properties (Only used with Store (not with Signal)) | Signal | TargetType @@ -218,7 +218,7 @@ export class Signal implements ISignal { // prevent accidental use as value valueOf() { if (qDev) { - return throwErrorAndStop('Cannot coerce a Signal, use `.value` instead'); + throw qError(QError.cannotCoerceSignal); } } @@ -270,7 +270,7 @@ export const ensureEffectContainsSubscriber = ( } effect.$effectDependencies$.push(subscriber); - } else if (vnode_isVNode(effect) && vnode_isVirtualVNode(effect)) { + } else if (vnode_isVNode(effect) && !vnode_isTextVNode(effect)) { let subscribers = vnode_getProp( effect, QSubscribers, @@ -340,13 +340,8 @@ export const triggerEffects = ( container.$scheduler$(ChoreType.QRL_RESOLVE, null, effect.$computeQrl$); } } - try { - retryOnPromise(() => - (effect as ComputedSignal | WrappedSignal).$invalidate$() - ); - } catch (e) { - logError(e); - } + + (effect as ComputedSignal | WrappedSignal).$invalidate$(); } else if (property === EffectProperty.COMPONENT) { const host: HostElement = effect as any; const qrl = container.getHostProp>>(host, OnRenderProp); @@ -359,10 +354,9 @@ export const triggerEffects = ( container.$scheduler$(ChoreType.NODE_DIFF, host, target, signal as Signal); } else { const host: HostElement = effect as any; - let effectData = effectSubscriptions[EffectSubscriptionsProp.FIRST_BACK_REF_OR_DATA]; - if (effectData instanceof EffectData) { - effectData = effectData as EffectData; - const data = effectData.data as NodePropData; + const effectData = effectSubscriptions[EffectSubscriptionsProp.FIRST_BACK_REF_OR_DATA]; + if (effectData instanceof EffectPropData) { + const data = effectData.data; const payload: NodePropPayload = { ...data, $value$: signal as Signal, @@ -393,6 +387,7 @@ export class ComputedSignal extends Signal { // We need a separate flag to know when the computation needs running because // we need the old value to know if effects need running after computation $invalid$: boolean = true; + $forceRunEffects$: boolean = false; constructor(container: Container | null, fn: QRLInternal<() => T>) { // The value is used for comparison when signals trigger, which can only happen @@ -403,15 +398,10 @@ export class ComputedSignal extends Signal { $invalidate$() { this.$invalid$ = true; - if (!this.$effects$?.length) { - return; - } + this.$forceRunEffects$ = false; // We should only call subscribers if the calculation actually changed. // Therefore, we need to calculate the value now. - // TODO move this calculation to the beginning of the next tick, add chores to that tick if necessary. New chore type? - if (this.$computeIfNeeded$()) { - triggerEffects(this.$container$, this, this.$effects$); - } + this.$container$?.$scheduler$(ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, null, this); } /** @@ -420,16 +410,20 @@ export class ComputedSignal extends Signal { */ force() { this.$invalid$ = true; + this.$forceRunEffects$ = false; triggerEffects(this.$container$, this, this.$effects$); } get untrackedValue() { - this.$computeIfNeeded$(); + const didChange = this.$computeIfNeeded$(); + if (didChange) { + this.$forceRunEffects$ = didChange; + } assertFalse(this.$untrackedValue$ === NEEDS_COMPUTATION, 'Invalid state'); return this.$untrackedValue$; } - private $computeIfNeeded$() { + $computeIfNeeded$() { if (!this.$invalid$) { return false; } @@ -442,9 +436,10 @@ export class ComputedSignal extends Signal { try { const untrackedValue = computeQrl.getFn(ctx)() as T; if (isPromise(untrackedValue)) { - throwErrorAndStop( - `useComputedSignal$ QRL ${computeQrl.dev ? `${computeQrl.dev.file} ` : ''}${computeQrl.$hash$} returned a Promise` - ); + throw qError(QError.computedNotSync, [ + computeQrl.dev ? computeQrl.dev.file : '', + computeQrl.$hash$, + ]); } DEBUG && log('Signal.$compute$', untrackedValue); this.$invalid$ = false; @@ -467,7 +462,7 @@ export class ComputedSignal extends Signal { } set value(_: any) { - throwErrorAndStop('ComputedSignal is read-only'); + throw qError(QError.computedReadOnly); } } @@ -480,6 +475,8 @@ export class WrappedSignal extends Signal implements Subscriber { // we need the old value to know if effects need running after computation $invalid$: boolean = true; $effectDependencies$: Subscriber[] | null = null; + $hostElement$: HostElement | null = null; + $forceRunEffects$: boolean = false; constructor( container: Container | null, @@ -495,15 +492,14 @@ export class WrappedSignal extends Signal implements Subscriber { $invalidate$() { this.$invalid$ = true; - if (!this.$effects$?.length) { - return; - } + this.$forceRunEffects$ = false; // We should only call subscribers if the calculation actually changed. // Therefore, we need to calculate the value now. - // TODO move this calculation to the beginning of the next tick, add chores to that tick if necessary. New chore type? - if (this.$computeIfNeeded$()) { - triggerEffects(this.$container$, this, this.$effects$); - } + this.$container$?.$scheduler$( + ChoreType.RECOMPUTE_AND_SCHEDULE_EFFECTS, + this.$hostElement$, + this + ); } /** @@ -512,16 +508,20 @@ export class WrappedSignal extends Signal implements Subscriber { */ force() { this.$invalid$ = true; + this.$forceRunEffects$ = false; triggerEffects(this.$container$, this, this.$effects$); } get untrackedValue() { - this.$computeIfNeeded$(); + const didChange = this.$computeIfNeeded$(); + if (didChange) { + this.$forceRunEffects$ = didChange; + } assertFalse(this.$untrackedValue$ === NEEDS_COMPUTATION, 'Invalid state'); return this.$untrackedValue$; } - private $computeIfNeeded$() { + $computeIfNeeded$() { if (!this.$invalid$) { return false; } @@ -544,6 +544,6 @@ export class WrappedSignal extends Signal implements Subscriber { } set value(_: any) { - throwErrorAndStop('WrappedSignal is read-only'); + throw qError(QError.wrappedReadOnly); } } diff --git a/packages/qwik/src/core/ssr/ssr-render-jsx.ts b/packages/qwik/src/core/ssr/ssr-render-jsx.ts index 14ee2dbc449..f33eb6d2e45 100644 --- a/packages/qwik/src/core/ssr/ssr-render-jsx.ts +++ b/packages/qwik/src/core/ssr/ssr-render-jsx.ts @@ -7,10 +7,9 @@ import { Slot } from '../shared/jsx/slot.public'; import type { DevJSX, JSXNodeInternal, JSXOutput } from '../shared/jsx/types/jsx-node'; import type { JSXChildren } from '../shared/jsx/types/jsx-qwik-attributes'; import { SSRComment, SSRRaw, SSRStream, type SSRStreamChildren } from '../shared/jsx/utils.public'; -import { trackSignal } from '../use/use-core'; +import { trackSignalAndAssignHost } from '../use/use-core'; import { isAsyncGenerator } from '../shared/utils/async-generator'; import { EMPTY_ARRAY } from '../shared/utils/flyweight'; -import { throwErrorAndStop } from '../shared/utils/log'; import { ELEMENT_KEY, FLUSH_COMMENT, @@ -35,6 +34,7 @@ import { applyInlineComponent, applyQwikComponentBody } from './ssr-render-compo import type { ISsrComponentFrame, ISsrNode, SSRContainer, SsrAttrs } from './ssr-types'; import { qInspector } from '../shared/utils/qdev'; import { serializeAttribute } from '../shared/utils/styles'; +import { QError, qError } from '../shared/error/error'; class ParentComponentData { constructor( @@ -101,7 +101,7 @@ export function _walkJSX( } else if (typeof value === 'function') { if (value === Promise) { if (!options.allowPromises) { - return throwErrorAndStop('Promises not expected here.'); + throw qError(QError.promisesNotExpected); } (stack.pop() as Promise).then(resolveValue, rejectDrain); return; @@ -109,7 +109,7 @@ export function _walkJSX( const waitOn = (value as StackFn).apply(ssr); if (waitOn) { if (!options.allowPromises) { - return throwErrorAndStop('Promises not expected here.'); + throw qError(QError.promisesNotExpected); } waitOn.then(drain, rejectDrain); return; @@ -156,7 +156,7 @@ function processJSXNode( ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.WrappedSignal] : EMPTY_ARRAY); const signalNode = ssr.getLastNode(); enqueue(ssr.closeFragment); - enqueue(trackSignal(() => value.value as any, signalNode, EffectProperty.VNODE, ssr)); + enqueue(trackSignalAndAssignHost(value, signalNode, EffectProperty.VNODE, ssr)); } else if (isPromise(value)) { ssr.openFragment(isDev ? [DEBUG_TYPE, VirtualType.Awaited] : EMPTY_ARRAY); enqueue(ssr.closeFragment); @@ -534,7 +534,7 @@ function getSlotName(host: ISsrNode, jsx: JSXNodeInternal, ssr: SSRContainer): s if (constProps && typeof constProps == 'object' && 'name' in constProps) { const constValue = constProps.name; if (constValue instanceof WrappedSignal) { - return trackSignal(() => constValue.value, host, EffectProperty.COMPONENT, ssr); + return trackSignalAndAssignHost(constValue, host, EffectProperty.COMPONENT, ssr); } } return directGetPropsProxyProp(jsx, 'name') || QDefaultSlot; diff --git a/packages/qwik/src/core/tests/TEST.md b/packages/qwik/src/core/tests/TEST.md deleted file mode 100644 index 6c9391cd6ed..00000000000 --- a/packages/qwik/src/core/tests/TEST.md +++ /dev/null @@ -1,45 +0,0 @@ -- [x] Component - - - [x] `useSequentialScope()` - - [x] `useSignal()` - - [x] `useStore()` - - [x] `useTask$()` - - [x] `useResource$()` - - [x] `useComputed$()` - - [x] `useVisibleTask$()` - - [x] `useOn()`/`useOnDocument()`/`useOnWindow()` - - [x] `useContext()`/`useContextProvider()` - - [x] Projection / `` - - [x] `useStyle$()` - - [x] `useStyleScoped$()` - -- [ ] Serialization - - - [x] `undefined` - - [x] `REFERENCE` - - [x] `URL` - - [x] `Date` - - [x] `Regex` - - [x] `String` - - [x] `VNode` - - [x] `NaN` - - [x] `BigInt` - - [x] `Error` - - [x] `QRL` - - [x] `Task` - - [x] `Resource` - - [x] `Component` - - [x] `DerivedSignal` - - [x] `Signal` - - [ ] `SignalWrapper` - - [x] `Store` - - [x] `FormData` - - [x] `JSXNode` - - [x] `Set` - - [x] `Map` - - [x] `Promise` - -- Render API - - [x] `render()` - - [ ] `renderToString()` - - [ ] `renderToStream()` diff --git a/packages/qwik/src/core/tests/attributes.spec.tsx b/packages/qwik/src/core/tests/attributes.spec.tsx index 5dece8f1d28..07dfc480fb9 100644 --- a/packages/qwik/src/core/tests/attributes.spec.tsx +++ b/packages/qwik/src/core/tests/attributes.spec.tsx @@ -237,4 +237,69 @@ describe.each([ ); }); + + it('should add and remove attribute', async () => { + const Cmp = component$(() => { + const hide = useSignal(false); + const required = useSignal(true); + return ( + <> + + (required.value = !required.value)}> + {hide.value ? : } + + ); + }); + + const { vNode, document } = await render(, { debug }); + expect(vNode).toMatchVDOM( + + + + + + + + ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + + + + + + + + ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + + + + + + + + ); + + await trigger(document.body, 'span', 'click'); + + expect(vNode).toMatchVDOM( + + + + + + + + ); + }); }); diff --git a/packages/qwik/src/core/tests/component.spec.tsx b/packages/qwik/src/core/tests/component.spec.tsx index d7415111cdf..2273f36df79 100644 --- a/packages/qwik/src/core/tests/component.spec.tsx +++ b/packages/qwik/src/core/tests/component.spec.tsx @@ -20,10 +20,12 @@ import { type Signal as SignalType, } from '@qwik.dev/core'; import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { cleanupAttrs } from '../../testing/element-fixture'; -import { ErrorProvider } from '../../testing/rendering.unit-util'; import { delay } from '../shared/utils/promises'; +import { QError } from '../shared/error/error'; +import { ErrorProvider } from '../../testing/rendering.unit-util'; +import * as qError from '../shared/error/error'; const debug = false; //true; Error.stackTraceLimit = 100; @@ -185,19 +187,19 @@ describe.each([
Component 1 - 1 + 1
Component 1 - 1 + 1
Component 2 - 2 + 2
@@ -479,6 +481,7 @@ describe.each([ }); it('should not render textarea value for non-text value', async () => { + const qErrorSpy = vi.spyOn(qError, 'qError'); const Cmp = component$(() => { const signal = useSignal(

header

); return ( @@ -497,12 +500,9 @@ describe.each([ , { debug } ); - expect(ErrorProvider.error.message).toBe( - render === domRender ? 'The value of the textarea must be a string' : null - ); } catch (e) { - expect(render).toBe(ssrRenderToDom); - expect((e as Error).message).toBe('The value of the textarea must be a string'); + expect((e as Error).message).toBeDefined(); + expect(qErrorSpy).toHaveBeenCalledWith(QError.wrongTextareaValue); } }); diff --git a/packages/qwik/src/core/tests/projection.spec.tsx b/packages/qwik/src/core/tests/projection.spec.tsx index 78a1be5c02a..75962867266 100644 --- a/packages/qwik/src/core/tests/projection.spec.tsx +++ b/packages/qwik/src/core/tests/projection.spec.tsx @@ -101,11 +101,11 @@ describe.each([ }); const { vNode } = await render(, { debug: DEBUG }); expect(vNode).toMatchVDOM( - - - default-value - - + + + default-value + + ); }); it('should save default value in q:template if not used', async () => { @@ -258,6 +258,207 @@ describe.each([ ); }); + + it('should replace projection content with undefined', async () => { + const Test = component$(() => { + return ( +
+ +
+ ); + }); + + const Cmp = component$(() => { + const test = useSignal(1); + + return ( +
+ + {test.value ? ( +
+

Hello from Qwik

+
+ ) : undefined} +
+ + +
+ ); + }); + + const { vNode, document } = await render(, { debug: DEBUG }); + + expect(vNode).toMatchVDOM( + +
+ +
+ +
+

Hello from Qwik

+
+
+
+
+ +
+
+ ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + +
+ +
+ {''} +
+
+ +
+
+ ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + +
+ +
+ +
+

Hello from Qwik

+
+
+
+
+ +
+
+ ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + +
+ +
+ {''} +
+
+ +
+
+ ); + }); + + it('should replace projection content with null', async () => { + const Test = component$(() => { + return ( +
+ +
+ ); + }); + + const Cmp = component$(() => { + const test = useSignal(1); + + return ( +
+ + {test.value ? ( +
+

Hello from Qwik

+
+ ) : null} +
+ + +
+ ); + }); + + const { vNode, document } = await render(, { debug: DEBUG }); + + expect(vNode).toMatchVDOM( + +
+ +
+ +
+

Hello from Qwik

+
+
+
+
+ +
+
+ ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + +
+ +
+ {''} +
+
+ +
+
+ ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + +
+ +
+ +
+

Hello from Qwik

+
+
+
+
+ +
+
+ ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + +
+ +
+ {''} +
+
+ +
+
+ ); + }); + it('should ignore Slot inside inline-component', async () => { const Child = (props: { children: any }) => { return ( diff --git a/packages/qwik/src/core/tests/render-api.spec.tsx b/packages/qwik/src/core/tests/render-api.spec.tsx index 3950002de34..e787aadae2d 100644 --- a/packages/qwik/src/core/tests/render-api.spec.tsx +++ b/packages/qwik/src/core/tests/render-api.spec.tsx @@ -1121,7 +1121,7 @@ describe('render api', () => { stream, streaming, }); - expect(stream.write).toHaveBeenCalledTimes(6); + expect(stream.write).toHaveBeenCalledTimes(5); }); }); }); diff --git a/packages/qwik/src/core/tests/use-computed.spec.tsx b/packages/qwik/src/core/tests/use-computed.spec.tsx index a2abf59504a..dafe7a0e81b 100644 --- a/packages/qwik/src/core/tests/use-computed.spec.tsx +++ b/packages/qwik/src/core/tests/use-computed.spec.tsx @@ -15,7 +15,10 @@ import { useTask$, } from '@qwik.dev/core'; import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; +import { ErrorProvider } from '../../testing/rendering.unit-util'; +import { QError } from '../shared/error/error'; +import * as qError from '../shared/error/error'; const debug = false; //true; Error.stackTraceLimit = 100; @@ -218,6 +221,33 @@ describe.each([ ); }); + it('should disallow Promise in computed result', async () => { + const qErrorSpy = vi.spyOn(qError, 'qError'); + const Counter = component$(() => { + const count = useSignal(1); + const doubleCount = useComputed$(() => Promise.resolve(count.value * 2)); + return ( + + ); + }); + try { + await render( + + + , + { debug } + ); + } catch (e) { + expect((e as Error).message).toBeDefined(); + expect(qErrorSpy).toHaveBeenCalledWith(QError.computedNotSync, expect.any(Array)); + } + }); + describe('createComputed$', () => { it('can be created anywhere', async () => { const count = createSignal(1); @@ -440,22 +470,4 @@ describe.each([ true ); }); - - // TODO fix this: by throwing during render, this breaks the tests that follow - it('should disallow Promise in computed result', async () => { - const Counter = component$(() => { - const count = useSignal(1); - const doubleCount = useComputed$(() => Promise.resolve(count.value * 2)); - return ( - - ); - }); - - await expect(() => render(, { debug })).rejects.toThrowError(/Promise/); - }); }); diff --git a/packages/qwik/src/core/tests/use-signal.spec.tsx b/packages/qwik/src/core/tests/use-signal.spec.tsx index caedcdaec0b..8b8c03441ad 100644 --- a/packages/qwik/src/core/tests/use-signal.spec.tsx +++ b/packages/qwik/src/core/tests/use-signal.spec.tsx @@ -17,6 +17,7 @@ import { untrack } from '../use/use-core'; import { useSignal } from '../use/use-signal'; import { vnode_getFirstChild, vnode_getProp, vnode_locate } from '../client/vnode'; import { QSubscribers } from '../shared/utils/markers'; +import { EffectSubscriptionsProp } from '../signal/signal'; const debug = false; //true; Error.stackTraceLimit = 100; @@ -273,38 +274,6 @@ describe.each([ ); }); - it("should don't add multiple the same subscribers", async () => { - const Child = component$(() => { - return <>; - }); - - const Cmp = component$(() => { - const counter = useSignal(0); - const cleanupCounter = useSignal(0); - - return ( - <> - - -
{cleanupCounter.value + ''}
- - ); - }); - - const { container } = await render(, { debug }); - - await trigger(container.element, 'button', 'click'); - await trigger(container.element, 'button', 'click'); - await trigger(container.element, 'button', 'click'); - await trigger(container.element, 'button', 'click'); - - const signalVNode = vnode_getFirstChild( - vnode_locate(container.rootVNode, container.element.querySelector('pre')!) - )!; - const subscribers = vnode_getProp(signalVNode, QSubscribers, null); - expect(subscribers).toHaveLength(1); - }); - it('should deserialize signal without effects', async () => { const Cmp = component$(() => { const counter = useSignal(0); @@ -325,6 +294,92 @@ describe.each([ ); }); + describe('signals cleanup', () => { + it('should not add multiple same subscribers for virtual node', async () => { + const Child = component$(() => { + return <>; + }); + + const Cmp = component$(() => { + const counter = useSignal(0); + const cleanupCounter = useSignal(0); + + return ( + <> + + +
{cleanupCounter.value + ''}
+ + ); + }); + + const { container } = await render(, { debug }); + + await trigger(container.element, 'button', 'click'); + await trigger(container.element, 'button', 'click'); + await trigger(container.element, 'button', 'click'); + await trigger(container.element, 'button', 'click'); + + const signalVNode = vnode_getFirstChild( + vnode_locate(container.rootVNode, container.element.querySelector('pre')!) + )!; + const subscribers = vnode_getProp(signalVNode, QSubscribers, null); + expect(subscribers).toHaveLength(1); + }); + + it('should not add multiple same subscribers for element node', async () => { + (globalThis as any).signal = undefined; + + const Cmp = component$(() => { + const show = useSignal(true); + const cleanupCounter = useSignal(0); + + useVisibleTask$(() => { + untrack(() => ((globalThis as any).signal = cleanupCounter)); + }); + + return ( +
+ + {show.value &&
}
+          
+ ); + }); + + const { container } = await render(, { debug }); + + if (render === ssrRenderToDom) { + await trigger(container.element, 'div', 'qvisible'); + } + + expect( + // wrapped signal on the pre element + (globalThis as any).signal.$effects$[0][EffectSubscriptionsProp.EFFECT].$effects$ + ).toHaveLength(1); + expect((globalThis as any).signal.$effects$).toHaveLength(1); + + await trigger(container.element, 'button', 'click'); + expect((globalThis as any).signal.$effects$).toHaveLength(0); + + await trigger(container.element, 'button', 'click'); // <-- this should not add another subscriber + expect((globalThis as any).signal.$effects$).toHaveLength(1); + expect( + (globalThis as any).signal.$effects$[0][EffectSubscriptionsProp.EFFECT].$effects$ + ).toHaveLength(1); + + await trigger(container.element, 'button', 'click'); + expect((globalThis as any).signal.$effects$).toHaveLength(0); + + await trigger(container.element, 'button', 'click'); // <-- this should not add another subscriber + expect((globalThis as any).signal.$effects$).toHaveLength(1); + expect( + (globalThis as any).signal.$effects$[0][EffectSubscriptionsProp.EFFECT].$effects$ + ).toHaveLength(1); + + (globalThis as any).signal = undefined; + }); + }); + describe('derived', () => { it('should update value directly in DOM', async () => { const log: string[] = []; diff --git a/packages/qwik/src/core/tests/use-store.spec.tsx b/packages/qwik/src/core/tests/use-store.spec.tsx index a03c3e37f2b..5115ca262bc 100644 --- a/packages/qwik/src/core/tests/use-store.spec.tsx +++ b/packages/qwik/src/core/tests/use-store.spec.tsx @@ -1,5 +1,6 @@ import { Fragment as Component, + Fragment as InlineComponent, component$, Fragment, Fragment as Signal, @@ -8,6 +9,7 @@ import { useStore, useTask$, useVisibleTask$, + type PropsOf, } from '@qwik.dev/core'; import { domRender, ssrRenderToDom, trigger } from '@qwik.dev/core/testing'; import { describe, expect, it, vi } from 'vitest'; @@ -285,6 +287,69 @@ describe.each([ ); }); + + it('should rerender inner component with store as a prop', async () => { + interface InnerButtonProps { + text: string; + isActive: boolean; + onClick$: PropsOf<'button'>['onClick$']; + } + const InnerButton = component$((props: InnerButtonProps) => { + return ( + + ); + }); + + const InnerButtonWrapper = component$((props: { data: any }) => { + return ( + { + props.data.selectedOutputDetail = 'options'; + }} + /> + ); + }); + + const Parent = component$(() => { + const store = useStore({ + selectedOutputDetail: 'console', + }); + + return ; + }); + + const { vNode, document } = await render(, { debug }); + + expect(vNode).toMatchVDOM( + + + + + + + + ); + + await trigger(document.body, 'button', 'click'); + + expect(vNode).toMatchVDOM( + + + + + + + + ); + }); }); describe('SerializationConstant at the start', () => { diff --git a/packages/qwik/src/core/use/use-context.ts b/packages/qwik/src/core/use/use-context.ts index d6c29d365aa..d02f50b77d9 100644 --- a/packages/qwik/src/core/use/use-context.ts +++ b/packages/qwik/src/core/use/use-context.ts @@ -1,5 +1,5 @@ import { assertTrue } from '../shared/error/assert'; -import { qError, QError_invalidContext, QError_notFoundContext } from '../shared/error/error'; +import { QError, qError } from '../shared/error/error'; import { verifySerializable } from '../shared/utils/serialize-utils'; import { qDev, qSerialize } from '../shared/utils/qdev'; import { isObject } from '../shared/utils/types'; @@ -276,11 +276,11 @@ export const useContext: UseContext = ( if (defaultValue !== undefined) { return set(defaultValue); } - throw qError(QError_notFoundContext, context.id); + throw qError(QError.notFoundContext, [context.id]); }; export const validateContext = (context: ContextId) => { if (!isObject(context) || typeof context.id !== 'string' || context.id.length === 0) { - throw qError(QError_invalidContext, context); + throw qError(QError.invalidContext, [context]); } }; diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index 331d7c339c2..db13350af5d 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -1,10 +1,6 @@ import type { QwikDocument } from '../document'; import { assertDefined } from '../shared/error/assert'; -import { - qError, - QError_useInvokeContext, - QError_useMethodOutsideContext, -} from '../shared/error/error'; +import { QError, qError } from '../shared/error/error'; import type { QRLInternal } from '../shared/qrl/qrl-class'; import type { QRL } from '../shared/qrl/qrl.public'; import { @@ -23,7 +19,13 @@ import type { Container, HostElement } from '../shared/types'; import { vnode_getNode, vnode_isElementVNode, vnode_isVNode } from '../client/vnode'; import { _getQContainerElement } from '../client/dom-container'; import type { ContainerElement } from '../client/types'; -import type { EffectData, EffectSubscriptions, EffectSubscriptionsProp } from '../signal/signal'; +import { + WrappedSignal, + type EffectPropData, + type EffectSubscriptions, + type EffectSubscriptionsProp, +} from '../signal/signal'; +import type { Signal } from '../signal/signal.public'; declare const document: QwikDocument; @@ -103,7 +105,7 @@ export const tryGetInvokeContext = (): InvokeContext | undefined => { export const getInvokeContext = (): InvokeContext => { const ctx = tryGetInvokeContext(); if (!ctx) { - throw qError(QError_useMethodOutsideContext); + throw qError(QError.useMethodOutsideContext); } return ctx; }; @@ -111,7 +113,7 @@ export const getInvokeContext = (): InvokeContext => { export const useInvokeContext = (): RenderInvokeContext => { const ctx = tryGetInvokeContext(); if (!ctx || ctx.$event$ !== RenderEvent) { - throw qError(QError_useInvokeContext); + throw qError(QError.useInvokeContext); } assertDefined(ctx.$hostElement$, `invoke: $hostElement$ must be defined`, ctx); assertDefined(ctx.$effectSubscriber$, `invoke: $effectSubscriber$ must be defined`, ctx); @@ -234,7 +236,7 @@ export const trackSignal = ( subscriber: EffectSubscriptions[EffectSubscriptionsProp.EFFECT], property: EffectSubscriptions[EffectSubscriptionsProp.PROPERTY], container: Container, - data?: EffectData + data?: EffectPropData ): T => { const previousSubscriber = trackInvocation.$effectSubscriber$; const previousContainer = trackInvocation.$container$; @@ -251,6 +253,19 @@ export const trackSignal = ( } }; +export const trackSignalAndAssignHost = ( + value: Signal, + host: HostElement, + property: EffectSubscriptions[EffectSubscriptionsProp.PROPERTY], + container: Container, + data?: EffectPropData +) => { + if (value instanceof WrappedSignal && value.$hostElement$ !== host && host) { + value.$hostElement$ = host; + } + return trackSignal(() => value.value, host, property, container, data); +}; + /** @internal */ export const _getContextElement = (): unknown => { const iCtx = tryGetInvokeContext(); diff --git a/packages/qwik/src/optimizer/core/src/const_replace.rs b/packages/qwik/src/optimizer/core/src/const_replace.rs index 8e448007b9c..6e905701954 100644 --- a/packages/qwik/src/optimizer/core/src/const_replace.rs +++ b/packages/qwik/src/optimizer/core/src/const_replace.rs @@ -9,6 +9,9 @@ pub struct ConstReplacerVisitor { pub is_server_ident: Option, pub is_browser_ident: Option, pub is_dev_ident: Option, + pub is_core_server_ident: Option, + pub is_core_browser_ident: Option, + pub is_core_dev_ident: Option, } impl ConstReplacerVisitor { @@ -21,6 +24,10 @@ impl ConstReplacerVisitor { is_browser_ident: global_collector .get_imported_local(&IS_BROWSER, &BUILDER_IO_QWIK_BUILD), is_dev_ident: global_collector.get_imported_local(&IS_DEV, &BUILDER_IO_QWIK_BUILD), + is_core_server_ident: global_collector.get_imported_local(&IS_SERVER, &BUILDER_IO_QWIK), + is_core_browser_ident: global_collector + .get_imported_local(&IS_BROWSER, &BUILDER_IO_QWIK), + is_core_dev_ident: global_collector.get_imported_local(&IS_DEV, &BUILDER_IO_QWIK), } } } @@ -50,6 +57,12 @@ impl VisitMut for ConstReplacerVisitor { ConstVariable::IsBrowser } else if id_eq!(ident, &self.is_dev_ident) { ConstVariable::IsDev + } else if id_eq!(ident, &self.is_core_server_ident) { + ConstVariable::IsServer + } else if id_eq!(ident, &self.is_core_browser_ident) { + ConstVariable::IsBrowser + } else if id_eq!(ident, &self.is_core_dev_ident) { + ConstVariable::IsDev } else { ConstVariable::None } diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_build_server.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_build_server.snap index 2526227d2a6..3b457f9954e 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_build_server.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_build_server.snap @@ -7,15 +7,15 @@ snapshot_kind: text ==INPUT== -import { component$, useStore } from '@qwik.dev/core'; -import { isServer, isBrowser } from '@qwik.dev/core/build'; +import { component$, useStore, isDev, isServer as isServer2 } from '@qwik.dev/core'; +import { isServer, isBrowser as isb } from '@qwik.dev/core/build'; import { mongodb } from 'mondodb'; import { threejs } from 'threejs'; import L from 'leaflet'; export const functionThatNeedsWindow = () => { - if (isBrowser) { + if (isb) { console.log('l', L); console.log('hey'); window.alert('hey'); @@ -27,14 +27,14 @@ export const App = component$(() => { if (isServer) { console.log('server', mongodb()); } - if (isBrowser) { + if (isb) { console.log('browser', new threejs()); } }); return ( - {isServer &&

server

} - {isBrowser &&

server

} + {isServer2 &&

server

} + {isb &&

server

}
); }); @@ -62,7 +62,7 @@ export const s_ckEPmXZlub0 = ()=>{ }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";6BAgB8B;IAC7B,UAAU;QAER,QAAQ,GAAG,CAAC,UAAU;IAKxB;IACA,QACE,IAAI;GACJ,EAAc,EAAE,MAAM,EAAE,GAAG;GAC3B,OAA4B;EAC7B,EAAE;AAEJ\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";6BAgB8B;IAC7B,UAAU;QAER,QAAQ,GAAG,CAAC,UAAU;IAKxB;IACA,QACE,IAAI;GACJ,EAAe,EAAE,MAAM,EAAE,GAAG;GAC5B,OAAsB;EACvB,EAAE;AAEJ\"}") /* { "origin": "test.tsx", @@ -78,8 +78,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma "ctxName": "component$", "captures": false, "loc": [ - 381, - 632 + 412, + 652 ] } */ diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react.snap index e0fb059b2fd..2be8741fbc2 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react.snap @@ -9,7 +9,7 @@ snapshot_kind: text import { componentQrl, inlinedQrl, useLexicalScope, useHostElement, useStore, useTaskQrl, noSerialize, SkipRerender, implicit$FirstArg } from '@qwik.dev/core'; import { jsx, Fragment } from '@qwik.dev/core/jsx-runtime'; -import { isBrowser, isServer } from '@qwik.dev/core/build'; +import { isBrowser, isServer } from '@qwik.dev/core'; function qwikifyQrl(reactCmpQrl) { return /*#__PURE__*/ componentQrl(inlinedQrl((props)=>{ @@ -101,7 +101,7 @@ export { qwikify$, qwikifyQrl, renderToString }; ============================= ../node_modules/@qwik.dev/react/index.qwik.mjs_qwikifyQrl_component_useWatch_x04JC5xeP1U.mjs (ENTRY POINT)== import { _auto_filterProps as filterProps } from "./index.qwik.mjs"; -import { isBrowser } from "@qwik.dev/core/build"; +import { isBrowser } from "@qwik.dev/core"; import { noSerialize } from "@qwik.dev/core"; import { useLexicalScope } from "@qwik.dev/core"; export const qwikifyQrl_component_useWatch_x04JC5xeP1U = async (track)=>{ @@ -147,8 +147,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/react/inde "ctxName": "useTask$", "captures": true, "loc": [ - 642, - 1371 + 636, + 1365 ] } */ @@ -158,7 +158,7 @@ import { Fragment } from "@qwik.dev/core/jsx-runtime"; import { SkipRerender } from "@qwik.dev/core"; import { _jsxSorted } from "@qwik.dev/core"; import { _auto_filterProps as filterProps } from "./index.qwik.mjs"; -import { isServer } from "@qwik.dev/core/build"; +import { isServer } from "@qwik.dev/core"; import { qrl } from "@qwik.dev/core"; import { useHostElement } from "@qwik.dev/core"; import { useLexicalScope } from "@qwik.dev/core"; @@ -214,8 +214,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/react/inde "ctxName": "component$", "captures": true, "loc": [ - 364, - 2026 + 358, + 2020 ] } */ diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react_inline.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react_inline.snap index fe86458641b..26326d87921 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react_inline.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_qwik_react_inline.snap @@ -9,7 +9,7 @@ snapshot_kind: text import { componentQrl, inlinedQrl, useLexicalScope, useHostElement, useStore, useTaskQrl, noSerialize, SkipRerender, implicit$FirstArg } from '@qwik.dev/core'; import { jsx, Fragment } from '@qwik.dev/core/jsx-runtime'; -import { isBrowser, isServer } from '@qwik.dev/core/build'; +import { isBrowser, isServer } from '@qwik.dev/core'; function qwikifyQrl(reactCmpQrl) { return /*#__PURE__*/ componentQrl(inlinedQrl((props)=>{ @@ -103,7 +103,7 @@ export { qwikify$, qwikifyQrl, renderToString }; import { _jsxSorted } from "@qwik.dev/core"; import { componentQrl, inlinedQrl, useLexicalScope, useHostElement, useStore, useTaskQrl, noSerialize, SkipRerender, implicit$FirstArg } from '@qwik.dev/core'; import { Fragment } from '@qwik.dev/core/jsx-runtime'; -import { isBrowser, isServer } from '@qwik.dev/core/build'; +import { isBrowser, isServer } from '@qwik.dev/core'; function qwikifyQrl(reactCmpQrl) { return /*#__PURE__*/ componentQrl(/*#__PURE__*/ inlinedQrl((props)=>{ const [reactCmpQrl] = useLexicalScope(); @@ -187,7 +187,7 @@ export { qwikify$, qwikifyQrl, renderToString }; export { filterProps as _auto_filterProps }; -Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/react/index.qwik.mjs\"],\"names\":[],\"mappings\":\";AACA,SAAS,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,QAAQ,iBAAiB;AAC/J,SAAc,QAAQ,QAAQ,6BAA6B;AAC3D,SAAS,SAAS,EAAE,QAAQ,QAAQ,uBAAuB;AAE3D,SAAS,WAAW,WAAW;IAC9B,OAAO,WAAW,GAAG,sCAAwB,CAAC;QAC7C,MAAM,CAAC,YAAY,GAAG;QACtB,MAAM,cAAc;QACpB,MAAM,QAAQ,SAAS,CAAC;QACxB,IAAI;QACJ,IAAI,KAAK,CAAC,iBAAiB,EAAE,MAAM;aAC9B,IAAI,KAAK,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,EAAE,MAAM;QAC7D,oCAAsB,OAAO;YAC5B,MAAM,CAAC,aAAa,OAAO,aAAa,MAAM,GAAG;YACjD,MAAM;YACN,IAAI;gBACH,IAAI,MAAM,IAAI,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,YAAY;qBACrF;oBACJ,MAAM,CAAC,KAAK,OAAO,GAAG,MAAM,QAAQ,GAAG,CAAC;wBACvC,YAAY,OAAO;wBACnB,MAAM,CAAC;qBACP;oBACD,IAAI;oBACJ,IAAI,YAAY,iBAAiB,GAAG,GAAG,OAAO,OAAO,WAAW,CAAC,aAAa,OAAO,IAAI,CAAC,KAAK,YAAY,QAAQ,MAAM,KAAK;yBACzH;wBACJ,OAAO,OAAO,UAAU,CAAC;wBACzB,KAAK,MAAM,CAAC,OAAO,IAAI,CAAC,KAAK,YAAY;oBAC1C;oBACA,MAAM,IAAI,GAAG,YAAY;wBACxB;wBACA,KAAK;wBACL;oBACD;gBACD;;QAEF;;;;;YAKI;YACH;QACD;QACA,IAAI,YAAY,CAAC,KAAK,CAAC,cAAc,EAAE;YACtC,MAAM,QAAQ,QAAQ,GAAG,CAAC;gBACzB,YAAY,OAAO;gBACnB,MAAM,CAAC;aACP,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO;gBACrB,MAAM,OAAO,OAAO,MAAM,CAAC,KAAK,YAAY;gBAC5C,OAAO,WAAW,GAAG,WAAI;oBACxB,yBAAyB;oBACzB,CAAC,WAAW,EAAE;wBACb;qBACA;;YAEH;YACA,OAAO,WAAW,GAAG,WAAI,sBACd;QAEZ;QACA,OAAO,WAAW,GAAG,WAAI,kBACd,WAAW,GAAG,WAAI;IAE9B;;QAEI;QACH,SAAS;IACV;AACD;AACA,MAAM,cAAc,CAAC;IACpB,MAAM,MAAM,CAAC;IACb,OAAO,IAAI,CAAC,OAAO,OAAO,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,UAAU,CAAC,YAAY,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI;IACtD;IACA,OAAO;AACR;AACA,MAAM,WAAW,kBAAkB;AAEnC,eAAe,eAAe,QAAQ,EAAE,IAAI;IAC3C,MAAM,MAAM,MAAM,MAAM,CAAC;IACzB,MAAM,SAAS,MAAM,IAAI,cAAc,CAAC,UAAU;IAClD,MAAM,SAAS,IAAI,iBAAiB,CAAC,OAAO,IAAI;IAChD,MAAM,YAAY,SAAS,OAAO,IAAI;IACtC,OAAO;QACN,GAAG,MAAM;QACT,MAAM;IACP;AACD;AAEA,SAAS,QAAQ,EAAE,UAAU,EAAE,cAAc,GAAG\"}") +Some("{\"version\":3,\"sources\":[\"/user/qwik/node_modules/@qwik.dev/react/index.qwik.mjs\"],\"names\":[],\"mappings\":\";AACA,SAAS,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,iBAAiB,QAAQ,iBAAiB;AAC/J,SAAc,QAAQ,QAAQ,6BAA6B;AAC3D,SAAS,SAAS,EAAE,QAAQ,QAAQ,iBAAiB;AAErD,SAAS,WAAW,WAAW;IAC9B,OAAO,WAAW,GAAG,sCAAwB,CAAC;QAC7C,MAAM,CAAC,YAAY,GAAG;QACtB,MAAM,cAAc;QACpB,MAAM,QAAQ,SAAS,CAAC;QACxB,IAAI;QACJ,IAAI,KAAK,CAAC,iBAAiB,EAAE,MAAM;aAC9B,IAAI,KAAK,CAAC,cAAc,IAAI,KAAK,CAAC,cAAc,EAAE,MAAM;QAC7D,oCAAsB,OAAO;YAC5B,MAAM,CAAC,aAAa,OAAO,aAAa,MAAM,GAAG;YACjD,MAAM;YACN,IAAI;gBACH,IAAI,MAAM,IAAI,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,YAAY;qBACrF;oBACJ,MAAM,CAAC,KAAK,OAAO,GAAG,MAAM,QAAQ,GAAG,CAAC;wBACvC,YAAY,OAAO;wBACnB,MAAM,CAAC;qBACP;oBACD,IAAI;oBACJ,IAAI,YAAY,iBAAiB,GAAG,GAAG,OAAO,OAAO,WAAW,CAAC,aAAa,OAAO,IAAI,CAAC,KAAK,YAAY,QAAQ,MAAM,KAAK;yBACzH;wBACJ,OAAO,OAAO,UAAU,CAAC;wBACzB,KAAK,MAAM,CAAC,OAAO,IAAI,CAAC,KAAK,YAAY;oBAC1C;oBACA,MAAM,IAAI,GAAG,YAAY;wBACxB;wBACA,KAAK;wBACL;oBACD;gBACD;;QAEF;;;;;YAKI;YACH;QACD;QACA,IAAI,YAAY,CAAC,KAAK,CAAC,cAAc,EAAE;YACtC,MAAM,QAAQ,QAAQ,GAAG,CAAC;gBACzB,YAAY,OAAO;gBACnB,MAAM,CAAC;aACP,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO;gBACrB,MAAM,OAAO,OAAO,MAAM,CAAC,KAAK,YAAY;gBAC5C,OAAO,WAAW,GAAG,WAAI;oBACxB,yBAAyB;oBACzB,CAAC,WAAW,EAAE;wBACb;qBACA;;YAEH;YACA,OAAO,WAAW,GAAG,WAAI,sBACd;QAEZ;QACA,OAAO,WAAW,GAAG,WAAI,kBACd,WAAW,GAAG,WAAI;IAE9B;;QAEI;QACH,SAAS;IACV;AACD;AACA,MAAM,cAAc,CAAC;IACpB,MAAM,MAAM,CAAC;IACb,OAAO,IAAI,CAAC,OAAO,OAAO,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,UAAU,CAAC,YAAY,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI;IACtD;IACA,OAAO;AACR;AACA,MAAM,WAAW,kBAAkB;AAEnC,eAAe,eAAe,QAAQ,EAAE,IAAI;IAC3C,MAAM,MAAM,MAAM,MAAM,CAAC;IACzB,MAAM,SAAS,MAAM,IAAI,cAAc,CAAC,UAAU;IAClD,MAAM,SAAS,IAAI,iBAAiB,CAAC,OAAO,IAAI;IAChD,MAAM,YAAY,SAAS,OAAO,IAAI;IACtC,OAAO;QACN,GAAG,MAAM;QACT,MAAM;IACP;AACD;AAEA,SAAS,QAAQ,EAAE,UAAU,EAAE,cAAc,GAAG\"}") == DIAGNOSTICS == [] diff --git a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_strip_server_code.snap b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_strip_server_code.snap index c6954863711..e70aed85ab8 100644 --- a/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_strip_server_code.snap +++ b/packages/qwik/src/optimizer/core/src/snapshots/qwik_core__test__example_strip_server_code.snap @@ -8,7 +8,7 @@ snapshot_kind: text import { component$, serverLoader$, serverStuff$, $, client$, useStore, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { isServer } from '@qwik.dev/core'; import mongo from 'mongodb'; import redis from 'redis'; import { handler } from 'serverless'; @@ -78,8 +78,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma "ctxName": "useTask$", "captures": true, "loc": [ - 369, - 471 + 363, + 465 ] } */ @@ -115,8 +115,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma "ctxName": "$", "captures": false, "loc": [ - 544, - 593 + 538, + 587 ] } */ @@ -143,8 +143,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma "ctxName": "client$", "captures": false, "loc": [ - 616, - 670 + 610, + 664 ] } */ @@ -191,8 +191,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma "ctxName": "component$", "captures": false, "loc": [ - 285, - 841 + 279, + 835 ] } */ @@ -217,8 +217,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma "ctxName": "onClick$", "captures": false, "loc": [ - 781, - 808 + 775, + 802 ] } */ @@ -246,8 +246,8 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma "ctxName": "useTask$", "captures": false, "loc": [ - 730, - 750 + 724, + 744 ] } */ diff --git a/packages/qwik/src/optimizer/core/src/test.rs b/packages/qwik/src/optimizer/core/src/test.rs index f02a957c615..7401c13cba3 100644 --- a/packages/qwik/src/optimizer/core/src/test.rs +++ b/packages/qwik/src/optimizer/core/src/test.rs @@ -1822,7 +1822,7 @@ fn example_strip_server_code() { test_input!(TestInput { code: r#" import { component$, serverLoader$, serverStuff$, $, client$, useStore, useTask$ } from '@qwik.dev/core'; -import { isServer } from '@qwik.dev/core/build'; +import { isServer } from '@qwik.dev/core'; import mongo from 'mongodb'; import redis from 'redis'; import { handler } from 'serverless'; @@ -2730,15 +2730,15 @@ export const foo = () => console.log('foo'); fn example_build_server() { test_input!(TestInput { code: r#" -import { component$, useStore } from '@qwik.dev/core'; -import { isServer, isBrowser } from '@qwik.dev/core/build'; +import { component$, useStore, isDev, isServer as isServer2 } from '@qwik.dev/core'; +import { isServer, isBrowser as isb } from '@qwik.dev/core/build'; import { mongodb } from 'mondodb'; import { threejs } from 'threejs'; import L from 'leaflet'; export const functionThatNeedsWindow = () => { - if (isBrowser) { + if (isb) { console.log('l', L); console.log('hey'); window.alert('hey'); @@ -2750,14 +2750,14 @@ export const App = component$(() => { if (isServer) { console.log('server', mongodb()); } - if (isBrowser) { + if (isb) { console.log('browser', new threejs()); } }); return ( - {isServer &&

server

} - {isBrowser &&

server

} + {isServer2 &&

server

} + {isb &&

server

}
); }); @@ -3120,7 +3120,7 @@ fn example_qwik_react() { code: r#" import { componentQrl, inlinedQrl, useLexicalScope, useHostElement, useStore, useTaskQrl, noSerialize, SkipRerender, implicit$FirstArg } from '@qwik.dev/core'; import { jsx, Fragment } from '@qwik.dev/core/jsx-runtime'; -import { isBrowser, isServer } from '@qwik.dev/core/build'; +import { isBrowser, isServer } from '@qwik.dev/core'; function qwikifyQrl(reactCmpQrl) { return /*#__PURE__*/ componentQrl(inlinedQrl((props)=>{ @@ -3223,7 +3223,7 @@ fn example_qwik_react_inline() { code: r#" import { componentQrl, inlinedQrl, useLexicalScope, useHostElement, useStore, useTaskQrl, noSerialize, SkipRerender, implicit$FirstArg } from '@qwik.dev/core'; import { jsx, Fragment } from '@qwik.dev/core/jsx-runtime'; -import { isBrowser, isServer } from '@qwik.dev/core/build'; +import { isBrowser, isServer } from '@qwik.dev/core'; function qwikifyQrl(reactCmpQrl) { return /*#__PURE__*/ componentQrl(inlinedQrl((props)=>{ diff --git a/packages/qwik/src/server/qwik-copy.ts b/packages/qwik/src/server/qwik-copy.ts index 49157a615f4..908e16b4876 100644 --- a/packages/qwik/src/server/qwik-copy.ts +++ b/packages/qwik/src/server/qwik-copy.ts @@ -43,6 +43,7 @@ export { QDefaultSlot, Q_PROPS_SEPARATOR, NON_SERIALIZABLE_MARKER_PREFIX, + QSubscribers, } from '../core/shared/utils/markers'; export { maybeThen } from '../core/shared/utils/promises'; export { mapApp_remove, mapArray_get, mapArray_set } from '../core/client/vnode'; @@ -56,3 +57,4 @@ export { VNodeDataChar } from '../core/shared/vnode-data-types'; export { VNodeDataSeparator } from '../core/shared/vnode-data-types'; export { escapeHTML } from '../core/shared/utils/character-escaping'; export { getValidManifest } from '../optimizer/src/manifest'; +export { QError, qError } from '../core/shared/error/error'; diff --git a/packages/qwik/src/server/qwik-types.ts b/packages/qwik/src/server/qwik-types.ts index 710fc2749f5..37c16f083a1 100644 --- a/packages/qwik/src/server/qwik-types.ts +++ b/packages/qwik/src/server/qwik-types.ts @@ -14,7 +14,7 @@ export type { CorePlatformServer } from '../core/shared/platform/types'; export type { QRLInternal } from '../core/shared/qrl/qrl-class'; -export type { JSXOutput } from '../core/shared/jsx/types/jsx-node'; +export type { JSXOutput, JSXNodeInternal } from '../core/shared/jsx/types/jsx-node'; export type { JSXChildren } from '../core/shared/jsx/types/jsx-qwik-attributes'; export type { ContextId } from '../core/use/use-context'; export type { ValueOrPromise } from '../core/shared/utils/types'; diff --git a/packages/qwik/src/server/ssr-container.ts b/packages/qwik/src/server/ssr-container.ts index 8a01156f10a..a777ef746e8 100644 --- a/packages/qwik/src/server/ssr-container.ts +++ b/packages/qwik/src/server/ssr-container.ts @@ -1,19 +1,17 @@ /** @file Public APIs for the SSR */ import { + _EffectData as EffectData, _SharedContainer, _jsxSorted, _jsxSplit, _walkJSX, isSignal, - _EffectData as EffectData, } from '@qwik.dev/core'; import { isDev } from '@qwik.dev/core/build'; import type { ResolvedManifest } from '@qwik.dev/core/optimizer'; -import { getQwikLoaderScript } from '@qwik.dev/core/server'; import { applyPrefetchImplementation2 } from './prefetch-implementation'; import { getPrefetchResources } from './prefetch-strategy'; import { - dangerouslySetInnerHTML, DEBUG_TYPE, ELEMENT_ID, ELEMENT_KEY, @@ -21,33 +19,37 @@ import { ELEMENT_SEQ, ELEMENT_SEQ_IDX, OnRenderProp, + QBaseAttr, + QContainerAttr, + QContainerValue, QCtxAttr, + QInstanceAttr, + QLocaleAttr, + QManifestHashAttr, + QRenderAttr, + QRuntimeAttr, QScopedStyle, QSlot, QSlotParent, QSlotRef, QStyle, - QContainerAttr, QTemplate, + QVersionAttr, + Q_PROPS_SEPARATOR, VNodeDataChar, + VNodeDataSeparator, VirtualType, convertStyleIdsToString, + dangerouslySetInnerHTML, + escapeHTML, + isClassAttr, mapArray_get, mapArray_set, maybeThen, serializeAttribute, - isClassAttr, - QContainerValue, - VNodeDataSeparator, - QRenderAttr, - QRuntimeAttr, - QVersionAttr, - QBaseAttr, - QLocaleAttr, - QManifestHashAttr, - QInstanceAttr, - escapeHTML, - Q_PROPS_SEPARATOR, + QSubscribers, + QError, + qError, } from './qwik-copy'; import { type ContextId, @@ -56,8 +58,8 @@ import { type ISsrComponentFrame, type ISsrNode, type JSXChildren, + type JSXNodeInternal, type JSXOutput, - type NodePropData, type SerializationContext, type SsrAttrKey, type SsrAttrValue, @@ -66,10 +68,8 @@ import { type SymbolToChunkResolver, type ValueOrPromise, } from './qwik-types'; -import { Q_FUNCS_PREFIX } from './ssr-render'; -import type { PrefetchResource, RenderOptions, RenderToStreamResult } from './types'; -import { createTimer } from './utils'; import { DomRef, SsrComponentFrame, SsrNode } from './ssr-node'; +import { Q_FUNCS_PREFIX } from './ssr-render'; import { TagNesting, allowedContent, @@ -77,20 +77,27 @@ import { isSelfClosingTag, isTagAllowed, } from './tag-nesting'; +import { + VNodeDataFlag, + type PrefetchResource, + type RenderOptions, + type RenderToStreamResult, +} from './types'; +import { createTimer } from './utils'; import { CLOSE_FRAGMENT, + WRITE_ELEMENT_ATTRS, OPEN_FRAGMENT, - VNodeDataFlag, encodeAsAlphanumeric, vNodeData_addTextSize, vNodeData_closeFragment, vNodeData_createSsrNodeReference, vNodeData_incrementElementCount, + vNodeData_openElement, vNodeData_openFragment, type VNodeData, } from './vnode-data'; -// eslint-disable-next-line @typescript-eslint/no-restricted-imports -import type { JSXNodeInternal } from '../core/shared/jsx/types/jsx-node'; +import { getQwikLoaderScript } from './scripts'; export interface SSRRenderOptions { locale?: string; @@ -243,14 +250,6 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { ensureProjectionResolved(host: HostElement): void {} - processJsx(host: HostElement, jsx: JSXOutput): void { - /** - * During SSR the output needs to be streamed. So we should never get here, because we can't - * process JSX out of order. - */ - throw new Error('Should not get here.'); - } - handleError(err: any, $host$: HostElement): void { throw err; } @@ -360,6 +359,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { } this.createAndPushFrame(elementName, this.depthFirstElementCount++, currentFile); + vNodeData_openElement(this.currentElementFrame!.vNodeData); this.write('<'); this.write(elementName); if (varAttrs) { @@ -649,7 +649,10 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { if (flag & VNodeDataFlag.REFERENCE) { this.write(VNodeDataSeparator.REFERENCE_CH); } - if (flag & (VNodeDataFlag.TEXT_DATA | VNodeDataFlag.VIRTUAL_NODE)) { + if ( + flag & + (VNodeDataFlag.TEXT_DATA | VNodeDataFlag.VIRTUAL_NODE | VNodeDataFlag.ELEMENT_NODE) + ) { let fragmentAttrs: SsrAttrs | null = null; /** * We keep track of how many virtual open/close fragments we have seen so far. Normally we @@ -675,6 +678,14 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { } depth--; this.write(VNodeDataChar.CLOSE_CHAR); + } else if (value === WRITE_ELEMENT_ATTRS) { + // this is executed only for VNodeDataFlag.ELEMENT_NODE and written as `|some encoded attrs here|` + if (fragmentAttrs && fragmentAttrs.length) { + this.write(VNodeDataChar.SEPARATOR_CHAR); + writeFragmentAttrs(this.write.bind(this), this.addRoot.bind(this), fragmentAttrs); + this.write(VNodeDataChar.SEPARATOR_CHAR); + fragmentAttrs = vNodeAttrsStack.pop()!; + } } else if (value >= 0) { // Text nodes get encoded as alphanumeric characters. this.write(encodeAsAlphanumeric(value)); @@ -736,6 +747,9 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { case ELEMENT_SEQ_IDX: write(VNodeDataChar.SEQ_IDX_CHAR); break; + case QSubscribers: + write(VNodeDataChar.SUBS_CHAR); + break; // Skipping `\` character for now because it is used for escaping. case QCtxAttr: write(VNodeDataChar.CONTEXT_CHAR); @@ -1111,7 +1125,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { if (isSSRUnsafeAttr(key)) { if (isDev) { - throw new Error('Attribute value is unsafe for SSR'); + throw qError(QError.unsafeAttr); } continue; } @@ -1131,12 +1145,14 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { } else if (typeof value === 'function') { value(new DomRef(lastNode)); continue; + } else { + throw qError(QError.invalidRefValue); } } if (isSignal(value)) { const lastNode = this.getLastNode(); - const signalData = new EffectData({ + const signalData = new EffectData({ $scopedStyleIdPrefix$: styleScopedId, $isConst$: isConst, }); @@ -1157,7 +1173,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { if (tag === 'textarea' && key === 'value') { if (typeof value !== 'string') { if (isDev) { - throw new Error('The value of the textarea must be a string'); + throw qError(QError.wrongTextareaValue); } continue; } @@ -1165,17 +1181,20 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { key = QContainerAttr; value = QContainerValue.TEXT; } - const serializedValue = serializeAttribute(key, value, styleScopedId); if (serializedValue != null && serializedValue !== false) { this.write(' '); this.write(key); - if (serializedValue !== true) { + if (typeof serializedValue === 'number') { + this.write('='); + const strValue = escapeHTML(serializedValue); + this.write(strValue); + this.write(''); + } else if (serializedValue !== true) { this.write('="'); const strValue = escapeHTML(String(serializedValue)); this.write(strValue); - this.write('"'); } } @@ -1198,7 +1217,7 @@ const isQwikStyleElement = (tag: string, attrs: SsrAttrs | null | undefined) => }; function newTagError(text: string) { - return new Error('SsrError(tag): ' + text); + return qError(QError.tagError, [text]); } function hasDestroy(obj: any): obj is { $destroy$(): void } { diff --git a/packages/qwik/src/server/types.ts b/packages/qwik/src/server/types.ts index 8eb26b8fc22..77be0bc0761 100644 --- a/packages/qwik/src/server/types.ts +++ b/packages/qwik/src/server/types.ts @@ -216,4 +216,24 @@ export type RenderToStream = (opts: RenderToStreamOptions) => Promise= 0) { refId += encodeAsAlphanumeric(childCount); } } - const type = stack[stack.length - 2] as SsrNodeType; - return new SsrNode(currentComponentNode, type, refId, fragmentAttrs, cleanupQueue, vNodeData); } + const type = stack[stack.length - 2] as SsrNodeType; + return new SsrNode(currentComponentNode, type, refId, fragmentAttrs, cleanupQueue, vNodeData); } /** diff --git a/packages/qwik/src/testing/vdom-diff.unit-util.ts b/packages/qwik/src/testing/vdom-diff.unit-util.ts index 84518b66ade..c037ea15a96 100644 --- a/packages/qwik/src/testing/vdom-diff.unit-util.ts +++ b/packages/qwik/src/testing/vdom-diff.unit-util.ts @@ -42,7 +42,13 @@ import { } from '../core/shared/utils/event-names'; import { createDocument } from './document'; import { isElement } from './html'; -import { QRenderAttr, Q_PROPS_SEPARATOR } from '../core/shared/utils/markers'; +import { + ELEMENT_ID, + ELEMENT_KEY, + QRenderAttr, + QSubscribers, + Q_PROPS_SEPARATOR, +} from '../core/shared/utils/markers'; expect.extend({ toMatchVDOM( @@ -77,6 +83,8 @@ expect.extend({ }, }); +const ignoredAttributes = [QSubscribers, ELEMENT_ID, '', Q_PROPS_SEPARATOR]; + function getContainerElement(vNode: _VNode) { let maybeParent: _VNode | null; do { @@ -150,7 +158,14 @@ function diffJsxVNode( const receivedElement = vnode_isElementVNode(received) ? (vnode_getNode(received) as Element) : null; - propsAdd(allProps, vnode_isElementVNode(received) ? vnode_getAttrKeys(received).sort() : []); + propsAdd( + allProps, + vnode_isElementVNode(received) + ? vnode_getAttrKeys(received) + .filter((key) => !ignoredAttributes.includes(key)) + .sort() + : [] + ); receivedElement && propsAdd(allProps, constPropsFromElement(receivedElement)); path.push(tagToString(expected.type)); @@ -167,7 +182,8 @@ function diffJsxVNode( vnode_getAttr(received, propLowerCased) || receivedElement?.getAttribute(prop) || receivedElement?.getAttribute(propLowerCased); - let expectedValue = prop === 'key' || prop === 'q:key' ? receivedValue : expected.props[prop]; + let expectedValue = + prop === 'key' || prop === ELEMENT_KEY ? receivedValue : expected.props[prop]; if (typeof receivedValue === 'boolean' || typeof receivedValue === 'number') { receivedValue = serializeBooleanOrNumberAttribute(receivedValue); } @@ -458,7 +474,7 @@ function constPropsFromElement(element: Element) { const props: string[] = []; for (let i = 0; i < element.attributes.length; i++) { const attr = element.attributes[i]; - if (attr.name !== '' && attr.name !== Q_PROPS_SEPARATOR) { + if (!ignoredAttributes.includes(attr.name)) { props.push(attr.name); } } diff --git a/scripts/api-docs.ts b/scripts/api-docs.ts index 39cc93a171a..70d91c3281c 100644 --- a/scripts/api-docs.ts +++ b/scripts/api-docs.ts @@ -211,8 +211,7 @@ async function createApiMarkdown(a: ApiData) { md.push(``); // sanitize / adjust output - const content = m.content - .replace(//g, '') + const content = removeHtmlComments(m.content) // .replace(//g, '' .replace(/\\#\\#\\# (\w+)/gm, '### $1') .replace(/\\\[/gm, '[') @@ -232,6 +231,15 @@ async function createApiMarkdown(a: ApiData) { return mdOutput; } +function removeHtmlComments(input: string): string { + let previous; + do { + previous = input; + input = input.replace(/