├── .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 |
2 |
3 |
4 |
5 |
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 |
2 |
3 | I am a SSR-ed Lit element
4 |
5 |
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 |
2 |
3 | I am a SSR-ed Lit element
4 | And so am I
5 |
6 |
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 |
2 | {{ message }}
3 |
4 |
5 |
13 |
--------------------------------------------------------------------------------
/playground/pages/attribute-testing.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | This element should render an empty string
4 |
5 |
6 |
7 |
10 |
--------------------------------------------------------------------------------
/playground/pages/with-provide-inject.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/playground/pages/vue-component-in-slot.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/playground/components/ComponentWithInject.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ injectedVariable }}
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/runtime/components/LitWrapperClient.vue:
--------------------------------------------------------------------------------
1 |
14 |
--------------------------------------------------------------------------------
/playground/components/ComponentWrappingLitElement.vue:
--------------------------------------------------------------------------------
1 |
2 | Button
3 | Button
4 |
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 |
2 |
3 |
4 | foo
5 | bar
6 | baz
7 |
8 |
9 |
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 |
2 |
7 |
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 |
2 |
3 |
4 |
5 |
6 | I am a Lit element within another Lit element
7 |
8 |
9 |
10 |
11 |
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 |
2 |
3 |
4 |
5 | {{ item.content }}
6 |
7 |
8 |
9 |
10 |
11 |
25 |
--------------------------------------------------------------------------------
/playground/pages/with-vue-bindings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Update Lit element from Vue
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {{ appendText }}
14 |
15 |
16 |
17 |
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 |
2 |
3 | I am a SSR-ed Lit element
4 |
5 |
6 |
7 |
8 |
9 |
10 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsum nesciunt quas voluptatem quis commodi
11 | excepturi, aspernatur quia doloribus suscipit sed illo, omnis quisquam sapiente iusto adipisci amet aperiam
12 | necessitatibus deserunt.
13 |
14 |
15 |
16 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Laudantium a quod facilis. Quam at doloremque, amet
17 | ullam earum iusto quia rem nihil impedit itaque labore vel neque repellat, inventore quaerat.
18 |
19 |
20 |
21 |
22 |
23 | and I am appended
24 |
25 |
26 |
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 | Lit button with name "${this.name}"
53 |
54 |
Default append text
55 |
`;
56 | }
57 | }
58 |
59 | customElements.define("my-element", MyElement);
60 |
--------------------------------------------------------------------------------
/playground/app.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Default
8 |
9 |
10 | With Vue bindings
11 |
12 |
13 | Nested Lit element in slot
14 |
15 |
16 | Vue component in slot
17 |
18 |
19 | Multiple different element tags
20 |
21 |
22 | A different element
23 |
24 |
25 | Testing different attributes
26 |
27 |
28 | With v-for
29 |
30 |
31 | With v-if
32 |
33 |
34 | With provide/inject
35 |
36 |
37 | Fallthrough attributes
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
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 |
72 | ${this.title}
73 | ${this.open ? "-" : "+"}
74 |
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 |
93 |
94 |
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 | '