diff --git a/.changeset/config.json b/.changeset/config.json index a109e23..a5e95f4 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,11 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["@qwikdev/astro-website", "astro-deno-demo", "astro-node-demo", "astro-demo"] + "ignore": [ + "@qwikdev/astro-website", + "astro-deno-demo", + "astro-node-demo", + "astro-demo", + "demo-lib" + ] } diff --git a/apps/demo/src/components/qwik/counter.tsx b/apps/demo/src/components/qwik/counter.tsx index 49b16ea..90597e2 100644 --- a/apps/demo/src/components/qwik/counter.tsx +++ b/apps/demo/src/components/qwik/counter.tsx @@ -4,10 +4,8 @@ export const Counter = component$<{ initial: number }>((props) => { const counter = useSignal(props.initial); return ( - <> - - + ); }); diff --git a/apps/node-demo/package.json b/apps/node-demo/package.json index 081cb7c..256c50f 100644 --- a/apps/node-demo/package.json +++ b/apps/node-demo/package.json @@ -20,6 +20,7 @@ "@types/react-dom": "^18.2.21", "astro": "^4.12.2", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "demo-lib": "workspace:*" } } diff --git a/apps/node-demo/src/pages/index.astro b/apps/node-demo/src/pages/index.astro index 8baeb19..23a52c9 100644 --- a/apps/node-demo/src/pages/index.astro +++ b/apps/node-demo/src/pages/index.astro @@ -1,6 +1,5 @@ --- -import { Counter } from "@components/qwik/counter"; -import { SayHi } from "@components/qwik/say-hi"; +import { Counter } from "demo-lib"; --- @@ -13,9 +12,7 @@ import { SayHi } from "@components/qwik/say-hi";
- Yo - John - test +
\ No newline at end of file diff --git a/apps/node-demo/tsconfig.json b/apps/node-demo/tsconfig.json index 1881c25..5f7cfdf 100644 --- a/apps/node-demo/tsconfig.json +++ b/apps/node-demo/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@components/*": ["./src/components/*"] + "@components/*": ["./src/components/*"], + "demo-lib": ["../../libs/demo-lib"] }, "jsx": "react-jsx", diff --git a/libs/demo-lib/.eslintignore b/libs/demo-lib/.eslintignore new file mode 100644 index 0000000..039dbd2 --- /dev/null +++ b/libs/demo-lib/.eslintignore @@ -0,0 +1,31 @@ +**/*.log +**/.DS_Store +*. +.vscode/settings.json +.history +.yarn +bazel-* +bazel-bin +bazel-out +bazel-qwik +bazel-testlogs +dist +dist-dev +lib +lib-types +etc +external +node_modules +temp +tsc-out +tsdoc-metadata.json +target +output +rollup.config.js +build +.cache +.vscode +.rollup.cache +dist +tsconfig.tsbuildinfo +vite.config.ts diff --git a/libs/demo-lib/.eslintrc.cjs b/libs/demo-lib/.eslintrc.cjs new file mode 100644 index 0000000..9677632 --- /dev/null +++ b/libs/demo-lib/.eslintrc.cjs @@ -0,0 +1,40 @@ +module.exports = { + root: true, + env: { + browser: true, + es2021: true, + node: true + }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:qwik/recommended" + ], + parser: "@typescript-eslint/parser", + parserOptions: { + tsconfigRootDir: __dirname, + project: ["./tsconfig.json"], + ecmaVersion: 2021, + sourceType: "module", + ecmaFeatures: { + jsx: true + } + }, + plugins: ["@typescript-eslint"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-inferrable-types": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-namespace": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/ban-ts-comment": "off", + "prefer-spread": "off", + "no-case-declarations": "off", + "no-console": "off", + "@typescript-eslint/no-unused-vars": ["error"] + } +}; diff --git a/libs/demo-lib/.gitignore b/libs/demo-lib/.gitignore new file mode 100644 index 0000000..e95b829 --- /dev/null +++ b/libs/demo-lib/.gitignore @@ -0,0 +1,38 @@ +# Build +/dist +/lib +/lib-types +/server + +# Development +node_modules + +# Cache +.cache +.mf +.vscode +.rollup.cache +tsconfig.tsbuildinfo + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Editor +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Yarn +.yarn/* +!.yarn/releases diff --git a/libs/demo-lib/.prettierignore b/libs/demo-lib/.prettierignore new file mode 100644 index 0000000..1592248 --- /dev/null +++ b/libs/demo-lib/.prettierignore @@ -0,0 +1,6 @@ +# Files Prettier should not format +**/*.log +**/.DS_Store +*. +dist +node_modules diff --git a/libs/demo-lib/README.md b/libs/demo-lib/README.md new file mode 100644 index 0000000..f2c583c --- /dev/null +++ b/libs/demo-lib/README.md @@ -0,0 +1,47 @@ +# Qwik Library ⚡️ + +- [Qwik Docs](https://qwik.dev/) +- [Discord](https://qwik.dev/chat) +- [Qwik on GitHub](https://github.com/QwikDev/qwik) +- [@QwikDev](https://twitter.com/QwikDev) +- [Vite](https://vitejs.dev/) +- [Partytown](https://partytown.builder.io/) +- [Mitosis](https://github.com/BuilderIO/mitosis) +- [Builder.io](https://www.builder.io/) + +--- + +## Project Structure + +Inside your project, you'll see the following directories and files: + +``` +├── public/ +│ └── ... +└── src/ + ├── components/ + │ └── ... + └── index.ts +``` + +- `src/components`: Recommended directory for components. + +- `index.ts`: The entry point of your component library, make sure all the public components are exported from this file. + +## Development + +Development mode uses [Vite's development server](https://vitejs.dev/). For Qwik during development, the `dev` command will also server-side render (SSR) the output. The client-side development modules are loaded by the browser. + +``` +pnpm dev +``` + +> Note: during dev mode, Vite will request many JS files, which does not represent a Qwik production build. + +## Production + +The production build should generate the production build of your component library in (./lib) and the typescript type definitions in (./lib-types). + +``` +pnpm build +``` diff --git a/libs/demo-lib/package.json b/libs/demo-lib/package.json new file mode 100644 index 0000000..463478a --- /dev/null +++ b/libs/demo-lib/package.json @@ -0,0 +1,50 @@ +{ + "name": "demo-lib", + "version": "0.0.1", + "description": "Create a Qwik library", + "main": "./lib/index.qwik.mjs", + "qwik": "./lib/index.qwik.mjs", + "types": "./lib-types/index.d.ts", + "exports": { + ".": { + "import": "./lib/index.qwik.mjs", + "require": "./lib/index.qwik.cjs", + "types": "./lib-types/index.d.ts" + } + }, + "files": ["lib", "lib-types"], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "private": false, + "type": "module", + "scripts": { + "build": "qwik build", + "build.lib": "vite build --mode lib", + "build.types": "tsc --emitDeclarationOnly", + "dev": "vite --mode ssr", + "dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force", + "fmt": "prettier --write .", + "fmt.check": "prettier --check .", + "lint": "eslint \"src/**/*.ts*\"", + "release": "np", + "start": "vite --open --mode ssr", + "test": "echo \"No test specified\" && exit 0", + "qwik": "qwik" + }, + "devDependencies": { + "@builder.io/qwik": "^1.7.3", + "@types/eslint": "^8.56.10", + "@types/node": "^20.12.7", + "@typescript-eslint/eslint-plugin": "^7.7.1", + "@typescript-eslint/parser": "^7.7.1", + "eslint": "^8.57.0", + "eslint-plugin-qwik": "latest", + "np": "^8.0.4", + "prettier": "^3.2.5", + "typescript": "5.4.5", + "undici": "*", + "vite": "^5.2.10", + "vite-tsconfig-paths": "^4.2.1" + } +} diff --git a/libs/demo-lib/src/components/counter/counter.tsx b/libs/demo-lib/src/components/counter/counter.tsx new file mode 100644 index 0000000..e23669d --- /dev/null +++ b/libs/demo-lib/src/components/counter/counter.tsx @@ -0,0 +1,16 @@ +import { component$, useSignal } from "@builder.io/qwik"; + +export const Counter = component$(() => { + const count = useSignal(0); + + return ( +
+

Count: {count.value}

+

+ +

+
+ ); +}); diff --git a/libs/demo-lib/src/components/logo/logo.tsx b/libs/demo-lib/src/components/logo/logo.tsx new file mode 100644 index 0000000..2a4ac27 --- /dev/null +++ b/libs/demo-lib/src/components/logo/logo.tsx @@ -0,0 +1,16 @@ +import { component$ } from "@builder.io/qwik"; + +export const Logo = component$(() => { + return ( +
+ + Qwik Logo + +
+ ); +}); diff --git a/libs/demo-lib/src/entry.dev.tsx b/libs/demo-lib/src/entry.dev.tsx new file mode 100644 index 0000000..940e37b --- /dev/null +++ b/libs/demo-lib/src/entry.dev.tsx @@ -0,0 +1,17 @@ +/* + * WHAT IS THIS FILE? + * + * Development entry point using only client-side modules: + * - Do not use this mode in production! + * - No SSR + * - No portion of the application is pre-rendered on the server. + * - All of the application is running eagerly in the browser. + * - More code is transferred to the browser than in SSR mode. + * - Optimizer/Serialization/Deserialization code is not exercised! + */ +import { type RenderOptions, render } from "@builder.io/qwik"; +import Root from "./root"; + +export default function (opts: RenderOptions) { + return render(document, , opts); +} diff --git a/libs/demo-lib/src/entry.ssr.tsx b/libs/demo-lib/src/entry.ssr.tsx new file mode 100644 index 0000000..357f807 --- /dev/null +++ b/libs/demo-lib/src/entry.ssr.tsx @@ -0,0 +1,22 @@ +/** + * WHAT IS THIS FILE? + * + * SSR entry point, in all cases the application is rendered outside the browser, this + * entry point will be the common one. + * + * - Server (express, cloudflare...) + * - npm run start + * - npm run preview + * - npm run build + * + */ +import { type RenderToStreamOptions, renderToStream } from "@builder.io/qwik/server"; +import { manifest } from "@qwik-client-manifest"; +import Root from "./root"; + +export default function (opts: RenderToStreamOptions) { + return renderToStream(, { + manifest, + ...opts + }); +} diff --git a/libs/demo-lib/src/index.ts b/libs/demo-lib/src/index.ts new file mode 100644 index 0000000..4ae1bfe --- /dev/null +++ b/libs/demo-lib/src/index.ts @@ -0,0 +1,2 @@ +export { Logo } from "./components/logo/logo"; +export { Counter } from "./components/counter/counter"; diff --git a/libs/demo-lib/src/root.tsx b/libs/demo-lib/src/root.tsx new file mode 100644 index 0000000..2850296 --- /dev/null +++ b/libs/demo-lib/src/root.tsx @@ -0,0 +1,17 @@ +import { Counter } from "./components/counter/counter"; +import { Logo } from "./components/logo/logo"; + +export default () => { + return ( + <> + + + Qwik Blank App + + + + + + + ); +}; diff --git a/libs/demo-lib/tsconfig.json b/libs/demo-lib/tsconfig.json new file mode 100644 index 0000000..b6cd919 --- /dev/null +++ b/libs/demo-lib/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "allowJs": true, + "target": "ES2017", + "module": "ES2020", + "lib": ["es2020", "DOM"], + "jsx": "react-jsx", + "jsxImportSource": "@builder.io/qwik", + "strict": true, + "declaration": true, + "declarationDir": "lib-types", + "resolveJsonModule": true, + "moduleResolution": "Bundler", + "esModuleInterop": true, + "skipLibCheck": true, + "incremental": true, + "isolatedModules": true, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/libs/demo-lib/vite.config.ts b/libs/demo-lib/vite.config.ts new file mode 100644 index 0000000..5f88c3a --- /dev/null +++ b/libs/demo-lib/vite.config.ts @@ -0,0 +1,30 @@ +import { qwikVite } from "@builder.io/qwik/optimizer"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import pkg from "./package.json"; + +const { dependencies = {}, peerDependencies = {} } = pkg as any; +const makeRegex = (dep) => new RegExp(`^${dep}(/.*)?$`); +const excludeAll = (obj) => Object.keys(obj).map(makeRegex); + +export default defineConfig(() => { + return { + build: { + target: "es2020", + lib: { + entry: "./src/index.ts", + formats: ["es", "cjs"], + fileName: (format) => `index.qwik.${format === "es" ? "mjs" : "cjs"}` + }, + rollupOptions: { + // externalize deps that shouldn't be bundled into the library + external: [ + /^node:.*/, + ...excludeAll(dependencies), + ...excludeAll(peerDependencies) + ] + } + }, + plugins: [qwikVite(), tsconfigPaths()] + }; +}); diff --git a/libs/qwikdev-astro/package.json b/libs/qwikdev-astro/package.json index 157a91b..6c398e9 100644 --- a/libs/qwikdev-astro/package.json +++ b/libs/qwikdev-astro/package.json @@ -12,6 +12,11 @@ "name": "Jack Shelton", "email": "me@jackshelton.com", "url": "https://twitter.com/TheJackShelton" + }, + { + "name": "Sigui Kessé Emmanuel", + "email": "dev@sikessem.com", + "url": "https://twitter.com/siguici" } ], "type": "module", @@ -44,7 +49,7 @@ }, "bugs": "https://github.com/thejackshelton/@qwikdev/astro/issues", "dependencies": { - "astro-integration-kit": "^0.2", + "astro-integration-kit": "^0.16", "fs-extra": "^11.1.1", "fs-move": "^6.0.0", "vite-tsconfig-paths": "^4.2.1" diff --git a/libs/qwikdev-astro/server.ts b/libs/qwikdev-astro/server.ts index 9e19f81..b10cd9c 100644 --- a/libs/qwikdev-astro/server.ts +++ b/libs/qwikdev-astro/server.ts @@ -8,19 +8,26 @@ import { } from "@builder.io/qwik"; import { isDev } from "@builder.io/qwik/build"; import type { QwikManifest } from "@builder.io/qwik/optimizer"; -import { getQwikLoaderScript, renderToString } from "@builder.io/qwik/server"; +import { + type RenderToStreamOptions, + getQwikLoaderScript, + renderToStream +} from "@builder.io/qwik/server"; import { manifest } from "@qwik-client-manifest"; -const qwikLoaderAdded = new WeakMap(); +const isQwikLoaderAddedMap = new WeakMap(); type RendererContext = { result: SSRResult; }; function isInlineComponent(component: unknown): boolean { + if (typeof component !== "function") return false; const codeStr = component!.toString().toLowerCase(); - - return codeStr.includes("_jsxq") || codeStr.includes("jsxSplit"); + return ( + (codeStr.includes("_jsxq") || codeStr.includes("jsxsplit")) && + component.name !== "QwikComponent" + ); } function isQwikComponent(component: unknown) { @@ -53,63 +60,76 @@ export async function renderToStaticMarkup( return; } - const isInline = isInlineComponent(component); const base = (props["q:base"] || process.env.Q_BASE) as string; - const renderConfig = { + + // html that gets added to the stream + let html = ""; + + const renderToStreamOpts: RenderToStreamOptions = { base, - containerTagName: "div", containerAttributes: { style: "display: contents" }, + containerTagName: "div", ...(isDev ? { manifest: {} as QwikManifest, - symbolMapper: (globalThis as any).symbolMapperGlobal + symbolMapper: globalThis.symbolMapperFn } : { manifest }), - qwikLoader: { include: "never" } - } as const; + serverData: props, + stream: { + write: (chunk) => { + html += chunk; + } + } + }; - // Handle inline components + // https://qwik.dev/docs/components/overview/#inline-components + const isInline = isInlineComponent(component); if (isInline) { const inlineComponentJSX = component(props); - - const result = await renderToString(inlineComponentJSX, renderConfig); - + // we don't want to process slots for inline components + await renderToStream(inlineComponentJSX, renderToStreamOpts); return { - html: result.html + html }; } - const shouldAddQwikLoader = !qwikLoaderAdded.has(this.result); + // https://qwik.dev/docs/advanced/qwikloader/#qwikloader + const isQwikLoaderNeeded = !isQwikLoaderAddedMap.has(this.result); const qwikLoader = - shouldAddQwikLoader && + isQwikLoaderNeeded && jsx("script", { "qwik-loader": "", dangerouslySetInnerHTML: getQwikLoaderScript() }); - // we want to add the sw script only on the first container. + /** + * service worker script is only added to the page once, and in prod. + * https://github.com/QwikDev/qwik/pull/5618 + */ const serviceWorkerScript = - !isDev && shouldAddQwikLoader && jsx(PrefetchServiceWorker, {}); - - // we want a prefetch graph on each container + !isDev && isQwikLoaderNeeded && jsx(PrefetchServiceWorker, {}); const prefetchGraph = !isDev && jsx(PrefetchGraph, {}); - - const slots: { [key: string]: unknown } = {}; - let defaultSlot: JSXNode<"span"> | undefined = undefined; - const qwikScripts = jsx("span", { "q:slot": "qwik-scripts", "qwik-scripts": "", children: [qwikLoader, serviceWorkerScript, prefetchGraph] }); - // this is how we get slots + const slots: { [key: string]: unknown } = {}; + let defaultSlot: JSXNode<"span"> | undefined = undefined; + + /** slot handling + * https://qwik.dev/docs/components/slots/#slots + * https://docs.astro.build/en/basics/astro-components/#slots + */ for (const [key, value] of Object.entries(slotted)) { + const namedSlot = key !== "default" && { "q:slot": key }; const jsxElement = jsx("span", { dangerouslySetInnerHTML: String(value), style: "display: contents", - ...(key !== "default" && { "q:slot": key }), - "q:key": Math.random().toString(26).split(".").pop() + ...namedSlot, + "q:key": globalThis.hash }); if (key === "default") { @@ -120,31 +140,26 @@ export async function renderToStaticMarkup( } const slotValues = Object.values(slots); - - const app = jsx(component, { + const qwikComponentJSX = jsx(component, { ...props, - children: [qwikScripts, ...(defaultSlot ? [defaultSlot] : []), ...slotValues] + children: [qwikScripts, defaultSlot, ...slotValues] }); - if (shouldAddQwikLoader) { - qwikLoaderAdded.set(this.result, true); + if (isQwikLoaderNeeded) { + isQwikLoaderAddedMap.set(this.result, true); } - // TODO: `jsx` must correctly be imported. - // Currently the vite loads `core.mjs` and `core.prod.mjs` at the same time and this causes issues. - // WORKAROUND: ensure that `npm postinstall` is run to patch the `@builder.io/qwik/package.json` file. - const result = await renderToString(app, renderConfig); - - const { html } = result; + await renderToStream(qwikComponentJSX, renderToStreamOpts); - // With VT, rerun so that signals work + /** With View Transitions, rerun so that signals work + * https://docs.astro.build/en/guides/view-transitions/#data-astro-rerun + */ const htmlWithRerun = html.replace( '