├── .eslintignore ├── commitlint.config.cjs ├── playground ├── wc │ ├── accordion │ │ ├── index.ts │ │ ├── accordion.ts │ │ ├── accordion-item.css │ │ └── accordion-item.ts │ ├── simple-parent.ts │ ├── my-element.ts │ └── simple-button.ts ├── pages │ ├── simple-parent.vue │ ├── simple-element.vue │ ├── multiple-different-element-tags.vue │ ├── attribute-testing.vue │ ├── with-provide-inject.vue │ ├── vue-component-in-slot.vue │ ├── with-v-if.vue │ ├── fallthrough-attributes.vue │ ├── nested-lit-element-in-slot.vue │ ├── with-v-for.vue │ ├── with-vue-bindings.vue │ └── index.vue ├── package.json ├── plugins │ └── custom-elements.ts ├── components │ ├── SimpleMessage.vue │ ├── ComponentWithInject.vue │ └── ComponentWrappingLitElement.vue ├── nuxt.config.ts ├── app.vue └── package-lock.json ├── .prettierignore ├── .husky └── commit-msg ├── tsconfig.json ├── prettier.config.cjs ├── src ├── runtime │ ├── plugins │ │ ├── hydrateSupport.client.ts │ │ ├── polyfill.client.ts │ │ ├── antiFouc.server.ts │ │ └── autoLitWrapper.ts │ ├── components │ │ ├── LitWrapperClient.vue │ │ ├── LitWrapper.vue │ │ └── LitWrapperServer.vue │ └── utils │ │ ├── customElements.ts │ │ └── litElementRenderer.ts └── module.ts ├── .nuxtrc ├── vitest.config.ts ├── .github ├── workflows │ ├── test.yml │ └── release-please.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .gitignore ├── .eslintrc.json ├── LICENSE ├── package.json ├── tests ├── playground │ └── basic.spec.ts └── module │ └── autoLitWrapper.spec.ts ├── README.md └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .nuxt 4 | .output 5 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /playground/wc/accordion/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./accordion"; 2 | export * from "./accordion-item"; 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage/ 3 | dist 4 | .nuxt 5 | .output 6 | __snapshots__/ 7 | *.mdx 8 | .github/ -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /playground/pages/simple-parent.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["nuxt/app"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "my-module-playground", 4 | "dependencies": { 5 | "lit": "^3.1.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | trailingComma: "none", 4 | singleQuote: false, 5 | endOfLine: "auto" 6 | }; 7 | -------------------------------------------------------------------------------- /playground/pages/simple-element.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/runtime/plugins/hydrateSupport.client.ts: -------------------------------------------------------------------------------- 1 | import "@lit-labs/ssr-client/lit-element-hydrate-support.js"; 2 | import { defineNuxtPlugin } from "#imports"; 3 | 4 | export default defineNuxtPlugin(() => {}); 5 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | imports.autoImport=false 2 | # enable TypeScript bundler module resolution - https://www.typescriptlang.org/docs/handbook/modules/reference.html#bundler 3 | experimental.typescriptBundlerResolution=true 4 | -------------------------------------------------------------------------------- /playground/pages/multiple-different-element-tags.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | deps: { 6 | inline: ["@nuxt/test-utils-edge"], 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /playground/plugins/custom-elements.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin } from "#app"; 2 | import "@/wc/my-element"; 3 | import "@/wc/simple-button"; 4 | import "@/wc/accordion"; 5 | import "@/wc/simple-parent"; 6 | 7 | export default defineNuxtPlugin(() => {}); 8 | -------------------------------------------------------------------------------- /playground/components/SimpleMessage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /playground/pages/attribute-testing.vue: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /playground/pages/with-provide-inject.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /playground/pages/vue-component-in-slot.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /playground/components/ComponentWithInject.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/runtime/components/LitWrapperClient.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /playground/components/ComponentWrappingLitElement.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /playground/wc/accordion/accordion.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from "lit"; 2 | import { customElement } from "lit/decorators.js"; 3 | 4 | export class Accordion extends LitElement { 5 | /** 6 | * Render 7 | */ 8 | render() { 9 | return html`
`; 10 | } 11 | } 12 | if (!customElements.get("my-accordion")) { 13 | customElement("my-accordion")(Accordion); 14 | } 15 | -------------------------------------------------------------------------------- /playground/pages/with-v-if.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /src/runtime/utils/customElements.ts: -------------------------------------------------------------------------------- 1 | export function isCustomElementTag(name) { 2 | return typeof name === "string" && /-/.test(name); 3 | } 4 | 5 | export function getCustomElementConstructor(name) { 6 | if (typeof customElements !== "undefined" && isCustomElementTag(name)) { 7 | return customElements.get(name) || null; 8 | } else if (typeof name === "function") { 9 | return name; 10 | } 11 | return null; 12 | } 13 | -------------------------------------------------------------------------------- /playground/pages/fallthrough-attributes.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /playground/wc/simple-parent.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from "lit"; 2 | 3 | export class SimpleParent extends LitElement { 4 | render() { 5 | return html`

parent

6 | `; 7 | } 8 | } 9 | customElements.define("simple-parent", SimpleParent); 10 | 11 | export class SimpleChild extends LitElement { 12 | render() { 13 | return html`

child

`; 14 | } 15 | } 16 | customElements.define("simple-child", SimpleChild); 17 | -------------------------------------------------------------------------------- /src/runtime/components/LitWrapper.vue: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /playground/pages/nested-lit-element-in-slot.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from "nuxt/config"; 2 | import NuxtSsrLit from ".."; 3 | 4 | export default defineNuxtConfig({ 5 | modules: [[NuxtSsrLit, { litElementPrefix: ["my-", "simple-"] }]], 6 | sourcemap: process.env.NODE_ENV === "test" ? false : { client: true, server: true }, 7 | compatibilityDate: "2024-11-05", 8 | hooks: { 9 | "vite:extendConfig": (config, { isServer }) => { 10 | if (isServer) { 11 | config.build.rollupOptions.output.preserveModules = false; 12 | } 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: PR Check 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build: 9 | name: Build packages 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Install dependencies 16 | run: npm ci 17 | 18 | - name: Install Nuxt 3 Playground dependencies 19 | run: cd playground && npm i 20 | 21 | - name: Prepare 22 | run: npm run dev:prepare 23 | 24 | - name: Build packages 25 | run: npm run build 26 | 27 | - name: Test Nuxt 3 28 | run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/runtime/plugins/polyfill.client.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin } from "#imports"; 2 | 3 | export default defineNuxtPlugin(async () => { 4 | // shadowRootMode is the standardized attribute. 5 | // Chromium 90-111 supported shadowRoot, so we check for that as well. 6 | const hasNativeDsd = 7 | Object.hasOwnProperty.call(HTMLTemplateElement.prototype, "shadowRootMode") || 8 | Object.hasOwnProperty.call(HTMLTemplateElement.prototype, "shadowRoot"); 9 | 10 | if (!hasNativeDsd) { 11 | const { hydrateShadowRoots } = await import("@webcomponents/template-shadowroot/template-shadowroot.js"); 12 | hydrateShadowRoots(document.body); 13 | document.body.removeAttribute("dsd-pending"); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | - Nuxt version: [e.g. 3.0.0] 27 | - Browser [e.g. chrome, safari] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /playground/wc/accordion/accordion-item.css: -------------------------------------------------------------------------------- 1 | .accordion-item__invoker { 2 | display: flex; 3 | justify-content: space-between; 4 | background: none; 5 | border: none; 6 | border-top: 1px solid black; 7 | color: black; 8 | cursor: pointer; 9 | font-size: 16px; 10 | font-weight: 700; 11 | line-height: 24px; 12 | padding: 20px 48px 16px 0; 13 | position: relative; 14 | text-align: left; 15 | width: 100%; 16 | } 17 | 18 | .accordion-item__invoker[aria-expanded='true'] { 19 | color: green; 20 | } 21 | 22 | .accordion-item__invoker:hover, 23 | .accordion-item__invoker.accordion-item__invoker[aria-expanded='true']:hover { 24 | color: blue; 25 | } 26 | 27 | .accordion-item__content:not([hidden]) { 28 | padding-bottom: 20px; 29 | } 30 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "vue/setup-compiler-macros": true 4 | }, 5 | "extends": [ 6 | "@nuxtjs/eslint-config-typescript", 7 | "eslint:recommended", 8 | "@vue/prettier", 9 | "@vue/typescript/recommended", 10 | "plugin:vue/vue3-recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-unused-vars": ["off"], 14 | "@typescript-eslint/no-empty-function": "off", 15 | "vue/no-deprecated-slot-attribute": "off", 16 | "vue/no-v-html": "off", 17 | "vue/attribute-hyphenation": "off", 18 | "vue/max-attributes-per-line": "off", 19 | "vue/html-self-closing": "off", 20 | "vue/singleline-html-element-content-newline": "off", 21 | "vue/html-closing-bracket-newline": "off", 22 | "vue/multiline-html-element-content-newline": "off", 23 | "vue/html-indent": "off" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /playground/pages/with-v-for.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | -------------------------------------------------------------------------------- /playground/pages/with-vue-bindings.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 36 | -------------------------------------------------------------------------------- /src/runtime/plugins/antiFouc.server.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtPlugin, useHead } from "#imports"; 2 | 3 | /** 4 | * This plugin is used to prevent the Flash Of Unstyled Content (FOUC) in browsers 5 | * that do not support native Declarative Shadow Dom (DSD). 6 | * 7 | * This is done by adding a "dsd-pending" attribute to the body element if the browser 8 | * does not support native DSD. This attribute hides the body element until the 9 | * polyfill has been loaded and applied. Thereby, preventing the FOUC. 10 | */ 11 | export default defineNuxtPlugin(() => { 12 | useHead({ 13 | style: [ 14 | { 15 | innerHTML: "body[dsd-pending] { display: none; }" 16 | } 17 | ], 18 | script: [ 19 | { 20 | children: ` 21 | { 22 | const hasNativeDsd = 23 | Object.hasOwnProperty.call(HTMLTemplateElement.prototype, "shadowRootMode") || 24 | Object.hasOwnProperty.call(HTMLTemplateElement.prototype, "shadowRoot"); 25 | 26 | if (!hasNativeDsd) { 27 | document.body.setAttribute('dsd-pending', 'true'); 28 | } 29 | }`, 30 | tagPosition: "bodyOpen" 31 | } 32 | ] 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | name: release-please 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: google-github-actions/release-please-action@v3 11 | id: release 12 | with: 13 | release-type: node 14 | package-name: nuxt-ssr-lit 15 | # The logic below handles the npm publication: 16 | - uses: actions/checkout@v2 17 | # these if statements ensure that a publication only occurs when 18 | # a new release is created: 19 | if: ${{ steps.release.outputs.release_created }} 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: 20 23 | registry-url: 'https://registry.npmjs.org' 24 | if: ${{ steps.release.outputs.release_created }} 25 | - run: npm ci 26 | if: ${{ steps.release.outputs.release_created }} 27 | - run: npm run dev:prepare 28 | if: ${{ steps.release.outputs.release_created }} 29 | - run: npm run build 30 | if: ${{ steps.release.outputs.release_created }} 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 34 | if: ${{ steps.release.outputs.release_created }} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present Nuxt-SSR-Lit Contributors 4 | - Prashant Palikhe ([@prashantpalikhe](https://github.com/@prashantpalikhe)) 5 | - Steve Workman ([@steveworkman](https://github.com/@steveworkman)) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /playground/wc/my-element.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html } from "lit"; 2 | 3 | export class MyElement extends LitElement { 4 | static properties = { 5 | name: { type: String }, 6 | theme: { type: String } 7 | }; 8 | 9 | static styles = css` 10 | .my-element { 11 | background-color: black; 12 | color: white; 13 | padding: 16px; 14 | } 15 | 16 | .my-element--light { 17 | background-color: gray; 18 | color: black; 19 | } 20 | 21 | button { 22 | background: wite; 23 | color: black; 24 | border-radius: 4px; 25 | border: none; 26 | padding: 8px; 27 | margin: 12px 0; 28 | cursor: pointer; 29 | } 30 | `; 31 | 32 | constructor() { 33 | super(); 34 | this.name = "default"; 35 | this.theme = "dark"; 36 | } 37 | 38 | onButtonClick() { 39 | console.log("Lit button clicked"); 40 | const event = new CustomEvent("my-event", { 41 | detail: { 42 | message: "Something important happened" 43 | } 44 | }); 45 | this.dispatchEvent(event); 46 | } 47 | 48 | render() { 49 | return html`
50 | Default prepend text 51 |
52 | 53 |
54 | Default append text 55 |
`; 56 | } 57 | } 58 | 59 | customElements.define("my-element", MyElement); 60 | -------------------------------------------------------------------------------- /playground/app.vue: -------------------------------------------------------------------------------- 1 | 48 | -------------------------------------------------------------------------------- /src/runtime/utils/litElementRenderer.ts: -------------------------------------------------------------------------------- 1 | import type { VNodeProps } from "vue"; 2 | import { LitElementRenderer } from "@lit-labs/ssr/lib/lit-element-renderer.js"; 3 | import { getCustomElementConstructor, isCustomElementTag } from "./customElements"; 4 | 5 | export function createLitElementRenderer(tagName: string, props: VNodeProps): LitElementRenderer | null { 6 | if (!isCustomElementTag(tagName)) { 7 | return null; 8 | } 9 | 10 | const renderer = new LitElementRenderer(tagName); 11 | attachPropsToRenderer(renderer, props); 12 | 13 | return renderer; 14 | } 15 | 16 | function attachPropsToRenderer(renderer: LitElementRenderer, props: VNodeProps): LitElementRenderer { 17 | const customElementConstructor = getCustomElementConstructor(renderer.tagName); 18 | 19 | if (props) { 20 | for (const [key, value] of Object.entries(props)) { 21 | // check if this is a reactive property 22 | if ( 23 | customElementConstructor !== null && 24 | typeof customElementConstructor !== "string" && 25 | key in customElementConstructor.prototype 26 | ) { 27 | const isBooleanProp = customElementConstructor.elementProperties.get(key)?.type === Boolean; 28 | 29 | if (isBooleanProp && value === "") { 30 | // handle key only boolean props e.g. 31 | renderer.setProperty(key, true); 32 | } else { 33 | renderer.setProperty(key, value); 34 | } 35 | } else { 36 | renderer.setAttribute(key, value as string); 37 | } 38 | } 39 | } 40 | 41 | return renderer; 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-ssr-lit", 3 | "version": "1.6.32", 4 | "license": "MIT", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "import": "./dist/module.mjs", 9 | "require": "./dist/module.cjs" 10 | } 11 | }, 12 | "main": "./dist/module.cjs", 13 | "types": "./dist/types.d.ts", 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "nuxt-module-build", 19 | "dev": "nuxi dev playground", 20 | "dev:build": "nuxi build playground", 21 | "dev:prepare": "nuxt-module-build --stub && nuxi prepare playground", 22 | "lint": "eslint \"src/**/*.*\" --fix", 23 | "test": "vitest run tests", 24 | "test:watch": "vitest" 25 | }, 26 | "dependencies": { 27 | "@lit-labs/ssr": "3.2.2", 28 | "@nuxt/kit": "^3.16.2", 29 | "@vue/compiler-core": "^3.5.13", 30 | "@vue/compiler-sfc": "^3.5.13", 31 | "@webcomponents/template-shadowroot": "^0.2.1", 32 | "magic-string": "^0.30.12", 33 | "ufo": "^1.5.4" 34 | }, 35 | "devDependencies": { 36 | "@commitlint/cli": "^17.2.0", 37 | "@commitlint/config-conventional": "^17.2.0", 38 | "@nuxt/module-builder": "^0.8.4", 39 | "@nuxt/schema": "^3.16.2", 40 | "@nuxt/test-utils-edge": "^3.8.0-28284309.b3d3d7f4", 41 | "@nuxtjs/eslint-config-typescript": "^12.1.0", 42 | "@vue/eslint-config-prettier": "^9.0.0", 43 | "@vue/eslint-config-typescript": "^12.0.0", 44 | "cheerio": "^1.0.0-rc.12", 45 | "eslint": "^8.56.0", 46 | "eslint-config-prettier": "^9.1.0", 47 | "eslint-plugin-prettier": "^5.1.2", 48 | "eslint-plugin-vue": "^9.19.2", 49 | "husky": "^8.0.3", 50 | "nuxt": "^3.16.2", 51 | "prettier": "^3.1.1", 52 | "typescript": "^5.3.3", 53 | "vitest": "^0.31.1" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { defineNuxtModule, addPlugin, createResolver, addVitePlugin, addComponent } from "@nuxt/kit"; 2 | import { name, version } from "../package.json"; 3 | import autoLitWrapper from "./runtime/plugins/autoLitWrapper"; 4 | 5 | export interface NuxtSsrLitOptions { 6 | litElementPrefix: string | string[]; 7 | } 8 | 9 | export default defineNuxtModule({ 10 | meta: { 11 | name, 12 | version, 13 | configKey: "ssrLit" 14 | }, 15 | defaults: { 16 | litElementPrefix: [] 17 | }, 18 | async setup(options, nuxt) { 19 | nuxt.options.nitro.moduleSideEffects = nuxt.options.nitro.moduleSideEffects || []; 20 | nuxt.options.nitro.moduleSideEffects.push("@lit-labs/ssr/lib/render-lit-html.js"); 21 | 22 | const { resolve } = createResolver(import.meta.url); 23 | 24 | addPlugin(resolve("./runtime/plugins/antiFouc.server")); 25 | addPlugin(resolve("./runtime/plugins/polyfill.client")); 26 | addPlugin(resolve("./runtime/plugins/hydrateSupport.client")); 27 | 28 | await addComponent({ 29 | name: "LitWrapper", 30 | filePath: resolve("./runtime/components/LitWrapper.vue") 31 | }); 32 | 33 | await addComponent({ 34 | name: "LitWrapperClient", 35 | filePath: resolve("./runtime/components/LitWrapperClient") 36 | }); 37 | 38 | await addComponent({ 39 | name: "LitWrapperServer", 40 | filePath: resolve("./runtime/components/LitWrapperServer") 41 | }); 42 | 43 | const isCustomElement = nuxt.options.vue.compilerOptions.isCustomElement || (() => false); 44 | nuxt.options.vue.compilerOptions.isCustomElement = (tag) => 45 | (Array.isArray(options.litElementPrefix) 46 | ? options.litElementPrefix.some((p) => tag.startsWith(p)) 47 | : tag.startsWith(options.litElementPrefix)) || isCustomElement(tag); 48 | 49 | addVitePlugin( 50 | autoLitWrapper({ 51 | litElementPrefix: options.litElementPrefix, 52 | sourcemap: !!nuxt.options.sourcemap.server || !!nuxt.options.sourcemap.client 53 | }) 54 | ); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /playground/wc/accordion/accordion-item.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html, unsafeCSS } from "lit"; 2 | import { customElement, property } from "lit/decorators.js"; 3 | import componentStyles from "./accordion-item.css?inline"; 4 | 5 | export class AccordionItem extends LitElement { 6 | static get properties() { 7 | return { 8 | title: { type: String, reflect: true }, 9 | open: { type: Boolean, reflect: true } 10 | }; 11 | } 12 | 13 | /** 14 | * Apply styles 15 | */ 16 | static get styles() { 17 | return [ 18 | css` 19 | ${unsafeCSS(componentStyles)} 20 | ` 21 | ]; 22 | } 23 | 24 | declare open: boolean; 25 | 26 | constructor() { 27 | super(); 28 | this.open = false; 29 | } 30 | 31 | /** 32 | * Methods 33 | */ 34 | show() { 35 | this.open = true; 36 | this.dispatchEvent(new CustomEvent("accordion-item-show", { bubbles: true })); 37 | } 38 | 39 | hide() { 40 | this.open = false; 41 | this.dispatchEvent(new CustomEvent("accordion-item-hide", { bubbles: true })); 42 | } 43 | 44 | toggle() { 45 | this.open ? this.hide() : this.show(); 46 | } 47 | 48 | _handleKeyDown(event: KeyboardEvent) { 49 | if (event.key === "ArrowDown" || event.key === "ArrowRight") { 50 | return this.show(); 51 | } 52 | 53 | if (event.key === "ArrowUp" || event.key === "ArrowLeft") { 54 | return this.hide(); 55 | } 56 | } 57 | 58 | /** 59 | * Render 60 | */ 61 | render() { 62 | return html`
63 | 75 | 76 |
77 | 78 |
79 |
`; 80 | } 81 | } 82 | if (!customElements.get("my-accordion-item")) { 83 | customElement("my-accordion-item")(AccordionItem); 84 | } 85 | -------------------------------------------------------------------------------- /src/runtime/components/LitWrapperServer.vue: -------------------------------------------------------------------------------- 1 | 67 | -------------------------------------------------------------------------------- /playground/wc/simple-button.ts: -------------------------------------------------------------------------------- 1 | import { css, html, LitElement, type PropertyDeclarations, type TemplateResult } from "lit"; 2 | import { customElement } from "lit/decorators.js"; 3 | 4 | const styles = css` 5 | :host { 6 | display: inline-flex; 7 | } 8 | 9 | :host(:focus) { 10 | outline: none; 11 | } 12 | 13 | :host([hidden]) { 14 | display: none; 15 | } 16 | 17 | :host button { 18 | align-items: center; 19 | background: red; 20 | border: 1px solid #919191; 21 | border-radius: 0.2rem; 22 | color: #fff; 23 | cursor: pointer; 24 | display: flex; 25 | font-family: sans-serif; 26 | outline: none; 27 | transition: all 0.4s; 28 | width: 100%; 29 | } 30 | 31 | :host button:hover { 32 | background: #5e5e5e; 33 | border-color: #5e5e5e; 34 | } 35 | 36 | :host button:active { 37 | background: #3b3b3b; 38 | border-color: #3b3b3b; 39 | } 40 | 41 | :host button:focus { 42 | border-color: white; 43 | box-shadow: 0 0 0 1px #919191, inset 0 0 0 1px #919191; 44 | } 45 | 46 | :host button:disabled { 47 | background: #8ac7fc; 48 | border-color: #8ac7fc; 49 | color: #919191; 50 | cursor: not-allowed; 51 | } 52 | `; 53 | 54 | export default class SimpleButton extends LitElement { 55 | static styles = styles; 56 | declare disabled; 57 | 58 | constructor() { 59 | super(); 60 | this.disabled = false; 61 | } 62 | 63 | static get properties(): PropertyDeclarations { 64 | return { 65 | disabled: { type: Boolean, reflect: true } 66 | }; 67 | } 68 | 69 | _button?: HTMLButtonElement | null; 70 | 71 | connectedCallback(): void { 72 | super.connectedCallback(); 73 | this.addEventListener("click", this); 74 | } 75 | 76 | disconnectedCallback(): void { 77 | super.disconnectedCallback(); 78 | this.removeEventListener("click", this); 79 | } 80 | 81 | firstUpdated(): void { 82 | this._button = this.querySelector("button"); 83 | } 84 | 85 | handleEvent(evt: Event): void { 86 | evt.stopPropagation(); 87 | this._button?.focus(); 88 | } 89 | 90 | render(): TemplateResult { 91 | return html` 92 | 95 | `; 96 | } 97 | } 98 | 99 | if (!customElements.get("simple-button")) { 100 | customElement("simple-button")(SimpleButton); 101 | } 102 | -------------------------------------------------------------------------------- /playground/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-module-playground", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "my-module-playground", 8 | "dependencies": { 9 | "lit": "^3.1.3" 10 | } 11 | }, 12 | "node_modules/@lit-labs/ssr-dom-shim": { 13 | "version": "1.2.0", 14 | "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", 15 | "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==" 16 | }, 17 | "node_modules/@lit/reactive-element": { 18 | "version": "2.0.4", 19 | "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", 20 | "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", 21 | "dependencies": { 22 | "@lit-labs/ssr-dom-shim": "^1.2.0" 23 | } 24 | }, 25 | "node_modules/@types/trusted-types": { 26 | "version": "2.0.7", 27 | "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", 28 | "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" 29 | }, 30 | "node_modules/lit": { 31 | "version": "3.1.3", 32 | "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.3.tgz", 33 | "integrity": "sha512-l4slfspEsnCcHVRTvaP7YnkTZEZggNFywLEIhQaGhYDczG+tu/vlgm/KaWIEjIp+ZyV20r2JnZctMb8LeLCG7Q==", 34 | "dependencies": { 35 | "@lit/reactive-element": "^2.0.4", 36 | "lit-element": "^4.0.4", 37 | "lit-html": "^3.1.2" 38 | } 39 | }, 40 | "node_modules/lit-element": { 41 | "version": "4.0.5", 42 | "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.5.tgz", 43 | "integrity": "sha512-iTWskWZEtn9SyEf4aBG6rKT8GABZMrTWop1+jopsEOgEcugcXJGKuX5bEbkq9qfzY+XB4MAgCaSPwnNpdsNQ3Q==", 44 | "dependencies": { 45 | "@lit-labs/ssr-dom-shim": "^1.2.0", 46 | "@lit/reactive-element": "^2.0.4", 47 | "lit-html": "^3.1.2" 48 | } 49 | }, 50 | "node_modules/lit-html": { 51 | "version": "3.1.3", 52 | "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.3.tgz", 53 | "integrity": "sha512-FwIbqDD8O/8lM4vUZ4KvQZjPPNx7V1VhT7vmRB8RBAO0AU6wuTVdoXiu2CivVjEGdugvcbPNBLtPE1y0ifplHA==", 54 | "dependencies": { 55 | "@types/trusted-types": "^2.0.2" 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/playground/basic.spec.ts: -------------------------------------------------------------------------------- 1 | // basic.test.js 2 | import { fileURLToPath } from "node:url"; 3 | import { describe, it, expect } from "vitest"; 4 | import { setup, $fetch } from "@nuxt/test-utils-edge"; 5 | import * as cheerio from "cheerio"; 6 | 7 | describe("ssr", async () => { 8 | await setup({ 9 | rootDir: fileURLToPath(new URL("../../playground", import.meta.url)) 10 | }); 11 | 12 | it("renders the index page with a single simple element", async () => { 13 | const html = await $fetch("/"); 14 | expect(html).toContain( 15 | '