├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── README.md ├── demo.html ├── package.json ├── postcss.config.js ├── src │ ├── app.css │ ├── lib │ │ ├── Counter.svelte │ │ ├── PokemonWidget.svelte │ │ ├── ShadowCounter.svelte │ │ └── shared │ │ │ ├── Container.svelte │ │ │ ├── Switch.svelte │ │ │ ├── Translator.svelte │ │ │ └── state.svelte.ts │ ├── main.ts │ └── vite-env.d.ts ├── svelte.config.js ├── tailwind.config.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── docs ├── .vitepress │ └── config.ts ├── demo.md ├── guide │ ├── backend-integration.md │ ├── custom-templates.md │ ├── faq.md │ └── quickstart.md ├── index.md ├── public │ ├── apple-touch-icon.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.svg │ ├── logo.png │ ├── web-app-manifest-192x192.png │ └── web-app-manifest-512x512.png ├── reference │ ├── component.md │ └── plugin.md └── what-is-svelte-anywhere.md ├── package-lock.json ├── package.json ├── release.config.cjs ├── src ├── index.ts └── templates │ ├── eager.template │ └── lazy.template └── tsconfig.json /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - next 7 | 8 | permissions: 9 | contents: write 10 | issues: write 11 | pull-requests: write 12 | pages: write 13 | id-token: write 14 | 15 | jobs: 16 | release: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: "lts/*" 23 | - run: npm ci 24 | - run: npm run build 25 | - run: npm audit signatures 26 | - name: Release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 30 | run: npx semantic-release 31 | 32 | build-docs: 33 | if: github.ref == 'refs/heads/main' # Only run on main branch 34 | needs: release 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout 38 | uses: actions/checkout@v4 39 | with: 40 | fetch-depth: 0 41 | - name: Setup Node 42 | uses: actions/setup-node@v4 43 | with: 44 | node-version: "lts/*" 45 | cache: npm 46 | - name: Setup Pages 47 | uses: actions/configure-pages@v4 48 | - name: Install dependencies 49 | run: npm ci 50 | - name: Build Demo Elements 51 | run: npm run demo:build 52 | - name: Build VitePress 53 | run: npm run docs:build 54 | - name: Upload artifact 55 | uses: actions/upload-pages-artifact@v3 56 | with: 57 | path: docs/.vitepress/dist 58 | 59 | deploy-docs: 60 | if: github.ref == 'refs/heads/main' # Only run on main branch 61 | environment: 62 | name: github-pages 63 | url: ${{ steps.deployment.outputs.page_url }} 64 | needs: build-docs 65 | runs-on: ubuntu-latest 66 | name: Deploy 67 | steps: 68 | - name: Deploy to GitHub Pages 69 | id: deployment 70 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /dist/ 3 | /node_modules/ 4 | /*/node_modules/ 5 | /docs/.vitepress/dist 6 | /docs/.vitepress/cache 7 | /docs/public/demo/ 8 | /demo/src/generated/custom-element/ 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Felix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](docs/public/logo.png) 2 | 3 | # Vite Plugin: Svelte Anywhere 4 | 5 | [![NPM Version](https://img.shields.io/npm/v/vite-plugin-svelte-anywhere)](https://www.npmjs.com/package/vite-plugin-svelte-anywhere) 6 | [![NPM Downloads](https://img.shields.io/npm/d18m/vite-plugin-svelte-anywhere)](https://img.shields.io/npm/d18m/vite-plugin-svelte-anywhere) 7 | [![Build Status](https://github.com/vidschofelix/vite-plugin-svelte-anywhere/actions/workflows/release.yml/badge.svg)](https://github.com/vidschofelix/vite-plugin-svelte-anywhere/actions) 8 | [![License](https://img.shields.io/github/license/vidschofelix/vite-plugin-svelte-anywhere)](https://github.com/vidschofelix/vite-plugin-svelte-anywhere/blob/main/LICENSE) 9 | 10 | **Use Svelte components anywhere HTML is accepted.** 11 | 12 | This Vite plugin lets you define **custom elements** right inside your Svelte components—just add an annotation, and you're ready to embed them in any environment, from static HTML to legacy backends or CMS platforms. 13 | 14 | No boilerplate. No runtime shenanigans. Just Svelte, anywhere. 15 | 16 | --- 17 | 18 | ### Features 19 | 20 | - 🧩 **Custom Elements from Svelte** — Turn any Svelte component into a reusable HTML element with a single comment. 21 | - 🛠 **Zero Boilerplate** — No manual registration or wrapper code. 22 | - 🔁 **Dev + Prod Ready** — Works with Vite HMR dev server and production builds 23 | - 🌓 **Shadow DOM Control** — Opt-in or out with simple config. 24 | - ⚡ **Lazy/Eager/Custom Templates** — Choose how your components are loaded. 25 | 26 | --- 27 | 28 | ### Links 29 | 30 | - 📚 [Documentation](https://svelte-anywhere.dev) 31 | - ✨ [Quickstart Guide](https://svelte-anywhere.dev/guide/quickstart) 32 | - 🎮 [Live Demo](https://svelte-anywhere.dev/demo) 33 | 34 | --- 35 | 36 | ### Quick Example 37 | 38 | ```svelte 39 | 40 | 43 | 44 | 47 | ``` 48 | 49 | Now just use it anywhere: 50 | ```html 51 | 52 | ``` 53 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Svelte + TS + Vite 2 | 3 | This template should help get you started developing with Svelte and TypeScript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). 8 | 9 | ## Need an official Svelte framework? 10 | 11 | Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. 12 | 13 | ## Technical considerations 14 | 15 | **Why use this over SvelteKit?** 16 | 17 | - It brings its own routing solution which might not be preferable for some users. 18 | - It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. 19 | 20 | This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. 21 | 22 | Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. 23 | 24 | **Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** 25 | 26 | Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. 27 | 28 | **Why include `.vscode/extensions.json`?** 29 | 30 | Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. 31 | 32 | **Why enable `allowJs` in the TS template?** 33 | 34 | While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. 35 | 36 | **Why is HMR not preserving my local component state?** 37 | 38 | HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). 39 | 40 | If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. 41 | 42 | ```ts 43 | // store.ts 44 | // An extremely simple external store 45 | import { writable } from 'svelte/store' 46 | export default writable(0) 47 | ``` 48 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" 11 | }, 12 | "devDependencies": { 13 | "@sveltejs/vite-plugin-svelte": "^6.1.1", 14 | "@tsconfig/svelte": "^5.0.4", 15 | "autoprefixer": "^10.4.20", 16 | "svelte": "^5.35.2", 17 | "svelte-check": "^4.1.1", 18 | "tailwindcss": "^3.4.17", 19 | "tailwindcss-scoped-preflight": "^3.4.10", 20 | "typescript": "^5.9.2", 21 | "vite": "^7.1.1", 22 | "vite-plugin-svelte-anywhere": "file:../src" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /demo/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /demo/src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | 5 | .twp { 6 | border-color: var(--vp-custom-block-details-border); 7 | color: var(--vp-custom-block-details-text); 8 | background-color: var(--vp-custom-block-details-bg); 9 | } -------------------------------------------------------------------------------- /demo/src/lib/Counter.svelte: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 | 14 |
-------------------------------------------------------------------------------- /demo/src/lib/PokemonWidget.svelte: -------------------------------------------------------------------------------- 1 | 2 | 24 | 25 |
26 |
27 | {#key pokemon?.name} 28 | {pokemon?.name} 32 | {/key} 33 |
34 | 35 |
36 | {#key pokemon?.name} 37 |

{pokemon?.name ?? "Loading"}

39 | {/key} 40 |
41 | 42 | 47 |
-------------------------------------------------------------------------------- /demo/src/lib/ShadowCounter.svelte: -------------------------------------------------------------------------------- 1 | 2 | 8 |
9 | 14 |
-------------------------------------------------------------------------------- /demo/src/lib/shared/Container.svelte: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |
8 | Number is {number} 9 | Translation: 10 |
-------------------------------------------------------------------------------- /demo/src/lib/shared/Switch.svelte: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 |
12 |
13 | English 14 | 18 | Spanish 19 |
20 |
-------------------------------------------------------------------------------- /demo/src/lib/shared/Translator.svelte: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | {numberTranslator.translate(number)} -------------------------------------------------------------------------------- /demo/src/lib/shared/state.svelte.ts: -------------------------------------------------------------------------------- 1 | type LanguageType = 'en' | 'es'; 2 | 3 | class NumberTranslator { 4 | language: LanguageType = $state('en') 5 | labels = [ 6 | {'en': 'zero', 'es': 'cero'}, 7 | {'en': 'one', 'es': 'uno'}, 8 | {'en': 'two', 'es': 'dos'}, 9 | {'en': 'three', 'es': 'tres'}, 10 | {'en': 'four', 'es': 'cuatro'}, 11 | {'en': 'five', 'es': 'cinco'}, 12 | {'en': 'six', 'es': 'seis'}, 13 | {'en': 'seven', 'es': 'siete'}, 14 | {'en': 'eight', 'es': 'ocho'}, 15 | {'en': 'nine', 'es': 'nueve'}, 16 | {'en': 'ten', 'es': 'diez'} 17 | ] 18 | 19 | getLanguage() { 20 | return this.language; 21 | } 22 | 23 | translate(number: number) { 24 | return this.labels[number][this.language] 25 | } 26 | 27 | toggleLanguage() { 28 | this.language = (this.language == 'en') ? 'es' : 'en'; 29 | } 30 | } 31 | 32 | export const numberTranslator = new NumberTranslator(); -------------------------------------------------------------------------------- /demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import.meta.glob('./generated/custom-element/*', { eager: true }); 2 | import './app.css' 3 | -------------------------------------------------------------------------------- /demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /demo/svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /demo/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | import {isolateInsideOfContainer, scopedPreflightStyles} from "tailwindcss-scoped-preflight"; 3 | 4 | export default { 5 | content: ['./src/**/*.{html,js,svelte,ts}'], 6 | 7 | theme: { 8 | extend: { 9 | } 10 | }, 11 | 12 | plugins: [ 13 | scopedPreflightStyles({ 14 | isolationStrategy: isolateInsideOfContainer('.twp', { 15 | except: '.no-twp', // optional, to exclude some elements under .twp from being preflighted, like external markup 16 | }), 17 | }), 18 | ] 19 | } satisfies Config; 20 | -------------------------------------------------------------------------------- /demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | "moduleDetection": "force" 18 | }, 19 | "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] 20 | } 21 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./../node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | import svelteAnywhere from "../src"; 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | svelteAnywhere({ 9 | log: true, 10 | }), 11 | svelte({ 12 | compilerOptions: { 13 | customElement: true, 14 | } 15 | }), 16 | ], 17 | base: '/demo', 18 | build: { 19 | target: "esnext", 20 | manifest: true, // Generate manifest.json for production // 21 | rollupOptions: { 22 | input: './src/main.ts', // Override default .html entry // 23 | }, 24 | outDir: '../docs/public/demo', 25 | emptyOutDir: true, 26 | }, 27 | server: { 28 | cors: true, 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /docs/.vitepress/config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig, HeadConfig} from 'vitepress' 2 | import {readFile} from "node:fs/promises"; 3 | import llmstxt from 'vitepress-plugin-llms' 4 | 5 | // https://vitepress.dev/reference/site-config 6 | 7 | const basePath = '' 8 | const demoBasePath = `${basePath}/demo` 9 | 10 | const headers_to_inject: Promise = (async () => { 11 | const headers: HeadConfig[] = [] 12 | const isDev = process.env.NODE_ENV !== 'production' 13 | 14 | if (isDev) { 15 | // Dev: HMR client + your demo entry live under the demo base 16 | headers.push(['script', { src: `http://localhost:5173${demoBasePath}/@vite/client`, type: 'module' }]) 17 | headers.push(['script', { src: `http://localhost:5173${demoBasePath}/src/main.ts`, type: 'module' }]) 18 | return headers 19 | } 20 | 21 | // Prod: read the demo’s manifest and inject built assets 22 | try { 23 | const manifest = JSON.parse( 24 | await readFile('docs/public/demo/.vite/manifest.json', 'utf8') 25 | ) 26 | 27 | const entry = manifest['src/main.ts'] 28 | if (entry?.file) { 29 | headers.push(['script', { src: `${demoBasePath}/${entry.file}`, type: 'module' }]) 30 | } 31 | 32 | entry?.css?.forEach((href: string) => { 33 | headers.push(['link', { rel: 'stylesheet', href: `${demoBasePath}/${href}` }]) // <-- 'link' (no space) 34 | }) 35 | } catch { 36 | // Don’t crash docs dev if the demo hasn’t been built yet 37 | headers.push([ 38 | 'script', 39 | {}, 40 | `console.warn('[docs] demo manifest not found; build the demo to inject prod assets.')` 41 | ]) 42 | } 43 | 44 | return headers 45 | })() 46 | 47 | export default defineConfig({ 48 | title: "Svelte Anywhere Docs", 49 | description: "Use Svelte components anywhere", 50 | head: [ 51 | ['meta', { property: 'og:type', content: 'website' }], 52 | ['meta', { property: 'og:locale', content: 'en' }], 53 | ['meta', { property: 'og:title', content: 'Svelte Anywhere | Use Svelte components anywhere' }], 54 | ['meta', { property: 'og:site_name', content: 'Svelte Anywhere' }], 55 | ['meta', { property: 'og:url', content: 'https://vidschofelix.github.io/vite-plugin-svelte-anywhere/' }], 56 | ['link', { rel: 'canonical', href: 'https://svelte-anywhere.dev/' }], 57 | ['link', { rel: 'shortcut icon', href: `${basePath}favicon.ico` }], 58 | ['link', { rel: 'icon', type: 'image/png', href: `${basePath}favicon-96x96.png`, sizes: '96x96'}], 59 | ['link', { rel: 'icon', type: 'image/svg+xml', href: `${basePath}favicon.svg`}], 60 | ['link', { rel: 'apple-touch-icon', sizes: '96x96', href: `${basePath}apple-touch-icon.png`}], 61 | 62 | ...await headers_to_inject 63 | ], 64 | vite:{ //in case you have issues with the page doing a full reload while working on the demo, uncomment this 65 | server: { 66 | port: 5170, 67 | hmr: true, 68 | }, 69 | plugins: [ 70 | llmstxt() as any 71 | ] 72 | }, 73 | themeConfig: { 74 | // https://vitepress.dev/reference/default-theme-config 75 | nav: [ 76 | { text: 'Home', link: '/' }, 77 | { text: 'Reference', link: '/reference/plugin' } 78 | ], 79 | logo: '/logo.png', 80 | 81 | sidebar: [ 82 | { 83 | text: 'Introduction', 84 | items: [ 85 | { text: 'What is Svelte Anywhere', link: '/what-is-svelte-anywhere'}, 86 | { text: 'Demos', link: '/demo'}, 87 | ] 88 | }, 89 | { 90 | text: 'Guides', 91 | items: [ 92 | { text: 'Quickstart', link: '/guide/quickstart' }, 93 | { text: 'FAQ', link: '/guide/faq' }, 94 | { text: 'Backend Integration', link: '/guide/backend-integration'}, 95 | { text: 'Using Custom Templates', link: '/guide/custom-templates' } 96 | ] 97 | }, 98 | { 99 | text: 'Reference', 100 | items: [ 101 | { text: 'Plugin Config', link: '/reference/plugin' }, 102 | { text: 'Component Config', link: '/reference/component' }, 103 | ] 104 | } 105 | ], 106 | search: { 107 | provider: 'local' 108 | }, 109 | // editLink: { 110 | // pattern: 'https://github.com/vidschofelix/vite-plugin-svelte-anywhere/tree/main/docs/:path', 111 | // text: 'Edit this page on GitHub' 112 | // }, 113 | socialLinks: [ 114 | { icon: 'github', link: 'https://github.com/vidschofelix/vite-plugin-svelte-anywhere' }, 115 | { icon: 'npm', link: 'https://www.npmjs.com/package/vite-plugin-svelte-anywhere' }, 116 | ], 117 | footer: { 118 | message: 'Released under the MIT License.', 119 | copyright: 'Copyright © 2024-present' 120 | }, 121 | }, 122 | vue: { 123 | template: { 124 | compilerOptions: { 125 | isCustomElement: (tag) => tag.includes('-'), //register custom components 126 | }, 127 | }, 128 | }, 129 | base: basePath 130 | }) 131 | -------------------------------------------------------------------------------- /docs/demo.md: -------------------------------------------------------------------------------- 1 | # Demos 2 | :::info 3 | All components of this page are svelte components, wrappend in custom elements (that's what the plugin does). 4 | The Page itself is a vue project. To get a better idea, have a look at the source codes provided and inspect the source code of this page. 5 | ::: 6 | 7 | ## Counter 8 | No Svelte Demo without the Counter! 9 | 10 | Embedded via `` 11 | 12 | ::: details source code 13 | :::code-group 14 | <<< @/../demo/src/lib/Counter.svelte{svelte} 15 | ::: 16 | 17 | ## Pokemon Widget 18 | Embedded via `` 19 | 20 | 21 | ::: details source code 22 | :::code-group 23 | <<< @/../demo/src/lib/PokemonWidget.svelte{svelte} 24 | ::: 25 | 26 | ## Number Translator 27 | Multiple custom components sharing state. 28 | 29 | Embedded via `` and `` 30 | 31 | 32 | 33 | 34 | 35 | ::: details source code 36 | :::code-group 37 | <<< @/../demo/src/lib/shared/state.svelte.ts{ts} 38 | <<< @/../demo/src/lib/shared/Switch.svelte{svelte} 39 | <<< @/../demo/src/lib/shared/Container.svelte{svelte} 40 | <<< @/../demo/src/lib/shared/Translator.svelte{svelte} 41 | ::: 42 | --- 43 | Using the same Components, you can also let Svelte components output just text. 44 | In this case, the listing is part of the Vue-Page, the words are Svelte. 45 | 46 | 47 | Embedded via `` and `` 48 | 49 | Lets count to 5: 50 | - 51 | - 52 | - 53 | - 54 | - 55 | 56 | Now change the language: 57 | 58 | 59 | 60 | 61 | 62 | ## Counter with Shadow-Mode Open 63 | Embedded via `` 64 | 65 | ::: details source code 66 | :::code-group 67 | <<< @/../demo/src/lib/ShadowCounter.svelte{svelte} 68 | ::: 69 | -------------------------------------------------------------------------------- /docs/guide/backend-integration.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 4] 3 | --- 4 | # Backend Integration 5 | 6 | There are several ways of adding files generated by Svelte Anywhere to your project 7 | 8 | ## Plugins 9 | - A lot of ready to use Backend Integrations can be found on [Awesome-Vite](https://github.com/vitejs/awesome-vite#integrations-with-backends) 10 | 11 | ## Manual 12 | ### Development 13 | Dev Environment is pretty easy. All you have to do is to add the vite client script and your entrypoint script to the head section of your page: 14 | ``` html [index.html] 15 | 16 | 17 | 18 | 19 | ``` 20 | 21 | ### Production 22 | 23 | Since we moved our entrypoint of our svelte components away from the `index.html`, into the `main.ts`, the compiler can no 24 | loger inject our entrypoint into our `index.html`. So we have to figure out, where to get our Svelte-App. It's a little complex, 25 | but it has a lot of advantages, like cacheable filenames and javascript chunks, that get loaded when needed. 26 | 27 | First you define your output-directory in your vite config. If you already have a directory that's served public, choose that. 28 | ::: code-group 29 | ``` ts [vite.config.ts] 30 | build: { 31 | manifest: true, 32 | rollupOptions: { 33 | input: './src/main.ts', 34 | }, 35 | outDir: '../public/svelte', 36 | emptyOutDir: true, 37 | } 38 | ``` 39 | ::: 40 | 41 | Next run `npm run build` once. It will create the folder, generate some js and css files and also a `.vite` folder. 42 | Inside you will find a `manifest.json` file, with all the files listed. You will need the "src/main.ts" file and css entries. 43 | 44 | An example looks like this: 45 | ``` json {3,14-16} 46 | ... 47 | "src/main.ts": { 48 | "file": "assets/main-DxdvDsYZ.js", 49 | "name": "main", 50 | "src": "src/main.ts", 51 | "isEntry": true, 52 | "dynamicImports": [ 53 | "src/lib/shared/Switch.svelte", 54 | "src/lib/PokemonWidget.svelte", 55 | "src/lib/ShadowCounter.svelte", 56 | "src/lib/shared/Translator.svelte", 57 | "src/lib/Counter.svelte" 58 | ], 59 | "css": [ 60 | "assets/main-CliQUYpN.css" 61 | ] 62 | ... 63 | ``` 64 | 65 | And now comes the hard part: Find a way to parse the manifest file and extract those infos. Then, for production, add the 66 | js-entry (`src/main.ts`) and prepend it with your public subdirectory (in this example /svelte). 67 | 68 | Do the same for the css-file-entries for the entrypoint file. It's an array, so loop over it. 69 | 70 | ## Examples 71 | ### PHP 72 | ``` php 73 | $useVite = false; 74 | if ($applicationVersion === 'development') { 75 | //test connection to vite, no need to throw warning 76 | $fp = @fsockopen('tcp://localhost', 5173, $errno, $errstr, 1); 77 | if ($fp) { 78 | $useVite = true; 79 | } 80 | } 81 | 82 | if ($useVite) { 83 | echo 84 | << 86 | 87 | HTML; 88 | } else { 89 | $manifest = file_get_contents(PROJECT_ROOT . '/public/svelte/.vite/manifest.json'); 90 | $manifest = json_decode($manifest, true); //decode json string to php associative array 91 | $svelteJs = "/svelte/" . $manifest['src/app.ts']['file']; 92 | $svelteCssFiles = $manifest['src/app.ts']['css']; //there are multiple 93 | echo ""; 94 | foreach ($svelteCssFiles as $file) { 95 | echo ""; 96 | }; 97 | } 98 | ?> 99 | ``` 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /docs/guide/custom-templates.md: -------------------------------------------------------------------------------- 1 | # Custom Templates 2 | 3 | ## Plugin provided templates 4 | By default the plugin comes with two templates: 5 | 6 | ::: code-group 7 | <<< @/../src/templates/lazy.template{js} 8 | <<< @/../src/templates/eager.template{js} 9 | ::: 10 | 11 | ::: info 12 | We suppress Sveltes [custom_element_props_identifier](https://svelte.dev/docs/svelte/compiler-warnings#custom_element_props_identifier) warning, because the generated custom component can't know wich props you will pass or expect in your component. By default all props will be passed down. 13 | ::: 14 | 15 | 16 | ## Creating your own Template 17 | If you want to use your own template follow these steps: 18 | ### Create folder 19 | For this example we will use `src/template` 20 | ### Define templatefolder in config 21 | :::code-group 22 | ``` ts [vite.config.ts] 23 | export default defineConfig({ 24 | plugins: [ 25 | svelteAnywhere({ 26 | templatesDir: './src/template' // [!code ++] 27 | }), 28 | ] 29 | }); 30 | ``` 31 | ::: 32 | 33 | ### Add your template 34 | Copy one of the templates and adjust to your needs 35 | :::code-group 36 | ``` js [src/template/dance.template] {6} 37 | 38 | 39 | 45 | 46 | 47 | ``` 48 | ::: 49 | 50 | The Plugin will replace {{CUSTOM_ELEMENT_TAG}}, 51 | {{SHADOW_MODE}} and {{SVELTE_PATH}} 52 | according to your component and annotation used. 53 | 54 | ### Use your template 55 | :::code-group 56 | ``` svelte [/src/lib/MyComponent.svelte] 57 | // [!code ++] 58 | 59 | ``` 60 | ::: 61 | You can use any of the templates, either your custom ones or still the provided ones. -------------------------------------------------------------------------------- /docs/guide/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ## When should I use this plugin? 4 | ### Use this plugin if: 5 | - You want to embed Svelte in a legacy or non-Svelte app (like PHP, Rails, WordPress, etc.) 6 | - You want simple, isolated UI widgets that drop into existing markup 7 | - You want to avoid full frontend rewrites 8 | ### Do not use this plugin if: 9 | - You're building a new Svelte app (just use [SvelteKit](https://svelte.dev/docs/kit) or Vite + Svelte) 10 | 11 | ## Can custom elements share state? 12 | Yes. Declare your shared state in a [.svelte.js or .svelte.ts file](https://svelte.dev/docs/svelte/svelte-js-files) 13 | and import it to your components. Look at the [Number Translator](/demo#number-translator) as an example. 14 | 15 | ## What does "anywhere" really mean? 16 | **Anywhere HTML is accepted**: legacy codebases, server-rendered sites, CMS platforms, even raw PHP or WordPress pages. If it can handle a ` 64 | 65 |
{message}
66 | ``` 67 | ::: 68 | The annotation creates a custom element `` that you can use anywhere. 69 | 70 | ### 4. Embed Custom Elements 71 | :::code-group 72 | ``` html [index.html] 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | ``` 84 | ::: 85 | 86 | Afterwards run `npm run dev` and visit your index.html. 87 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Svelte Anywhere" 7 | tagline: Use Svelte components anywhere. 8 | image: 9 | src: '/logo.png' 10 | alt: 'Svelte Anywhere Logo' 11 | actions: 12 | # - theme: brand 13 | # text: What is Svelte Anywhere? 14 | # link: /what-is-svelte-anywhere 15 | - theme: brand 16 | text: Quickstart 17 | link: /guide/quickstart 18 | - theme: alt 19 | text: Demo 20 | link: /demo 21 | - theme: alt 22 | text: GitHub 23 | link: https://github.com/vidschofelix/vite-plugin-svelte-anywhere 24 | 25 | features: 26 | - title: ⚡ Vite powered 27 | details: Use Svelte components in legacy or CMS-based projects 28 | - title: 🔥 HMR 29 | details: Hot module reloading in development 30 | - title: 💤 Lazy 31 | details: Lazy loading and bundle splitting for prod 32 | - title: 🧩 Templating 33 | details: Easily customizable templates 34 | --- 35 | 36 | [//]: # (# Svelte Anywhere Docs) 37 | -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidschofelix/vite-plugin-svelte-anywhere/bc160f9b04e5f3803614bc5a8990a4eb1e6876d4/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidschofelix/vite-plugin-svelte-anywhere/bc160f9b04e5f3803614bc5a8990a4eb1e6876d4/docs/public/favicon-96x96.png -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidschofelix/vite-plugin-svelte-anywhere/bc160f9b04e5f3803614bc5a8990a4eb1e6876d4/docs/public/favicon.ico -------------------------------------------------------------------------------- /docs/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidschofelix/vite-plugin-svelte-anywhere/bc160f9b04e5f3803614bc5a8990a4eb1e6876d4/docs/public/logo.png -------------------------------------------------------------------------------- /docs/public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidschofelix/vite-plugin-svelte-anywhere/bc160f9b04e5f3803614bc5a8990a4eb1e6876d4/docs/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /docs/public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidschofelix/vite-plugin-svelte-anywhere/bc160f9b04e5f3803614bc5a8990a4eb1e6876d4/docs/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /docs/reference/component.md: -------------------------------------------------------------------------------- 1 | 2 | # Component Config 3 | 4 | ## Full Reference 5 | ```js 6 | 7 | ``` 8 | 9 | ## Tag Name 10 | - **required** 11 | - should be lowercase 12 | - **must** contain at least one hyphen (`-`) 13 | - must not start with an Number 14 | - [more](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define#valid_custom_element_names) 15 | 16 | ## Template 17 | - optional 18 | - `lazy` or `eager` 19 | - default is set by [defaultTemplate](plugin.md#defaulttemplate) 20 | 21 | ## Shadow 22 | - optional 23 | - `open` or `none` 24 | - default is set by [Plugin Config](plugin.md#defaultshadowmode) -------------------------------------------------------------------------------- /docs/reference/plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: [2, 4] 3 | --- 4 | # Plugin Config 5 | 6 | ## Full Reference 7 | ::: code-group 8 | ```ts [vite.config.ts] 9 | export default defineConfig({ 10 | plugins: [ 11 | svelteAnywhere({ 12 | componentsDir: 'src', 13 | outputDir: 'src/generated/custom-element', 14 | defaultTemplate: 'lazy', 15 | defaultShadowMode: 'none', 16 | templatesDir: null, 17 | cleanOutputDir: true, 18 | log: true, 19 | }), 20 | svelte(), 21 | ] 22 | }); 23 | ``` 24 | ::: 25 | 26 | > [!IMPORTANT] 27 | > Make sure `svelteAnywhere` is positioned above the `svelte()`-plugin. 28 | 29 | ## Options 30 | ### componentsDir 31 | - type: string 32 | - default: `src` 33 | 34 | Directory where your Svelte components are located 35 | 36 | ### outputDir 37 | - type: string 38 | - default: `src/generated/custom-element` 39 | 40 | Directory where the custom elements are generated 41 | 42 | ### defaultTemplate 43 | - type: string 44 | - default: `'lazy'` 45 | 46 | The default template to use. If no templatesDir is provided must be 'lazy' or 'eager'. 47 | This can be overridden in [template annotation](component.md#template) 48 | 49 | ### defaultShadowMode 50 | - type: string 51 | - default: `'none'` 52 | 53 | ShadowDom Mode: 'open' or 'none'. Can be overridden in [shadow annotation](component.md#shadow) 54 | 55 | ### templatesDir 56 | - type: ?string 57 | - default: `null` 58 | - example: `'src/template'` 59 | 60 | Path to directory with custom templates. You can provide a directory in your codebase with custom templates. 61 | Those must be named `identifier.template` and can be used in the [template annotation](component.md#template) with `template=identifier` and also be set as [defaultTemplate](#defaulttemplate) 62 | 63 | Read more at the [Custom Template Guide](/guide/custom-templates.md) 64 | 65 | ### cleanOutputDir 66 | - type: boolean 67 | - default: `true` 68 | 69 | Whether to clean the `outputDir` on each build 70 | 71 | ### log 72 | - type: boolean 73 | - default: `false` 74 | 75 | Whether to enable logging, for debugging purposes 76 | -------------------------------------------------------------------------------- /docs/what-is-svelte-anywhere.md: -------------------------------------------------------------------------------- 1 | # What is Svelte-Anywhere 2 | **Svelte-Anywhere** is a Vite plugin that allows you to use Svelte Components **anywhere HTML is accepted**. Whether you're working on a modern web app, a legacy project, or even a CMS platform, Svelte-Anywhere makes it effortless to embed Svelte components into any environment—no rewrites, no hassle. 3 | 4 | ## Why Svelte-Anywhere 5 | 6 | - **Universal Compatibility**: Use Svelte components in server-rendered HTML, CMS platforms, or legacy projects. 7 | - **Custom Elements Made Simple**: Wrap Svelte components into reusable custom elements with just a single annotation. 8 | - **Dynamic & Scalable**: Enjoy hot module reloading during development and lazy loading for production. 9 | - **Shadow DOM Control**: Choose between open or no shadow DOM for encapsulation control. 10 | 11 | ## Downsides Of This Plugin 12 | - No excuse to not use Svelte anymore :wink: -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-svelte-anywhere", 3 | "version": "0.0.0-development", 4 | "description": "Use Svelte components anywhere", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/index.js", 10 | "require": "./dist/index.cjs" 11 | } 12 | }, 13 | "scripts": { 14 | "build": "tsup src/index.ts --format esm --dts && mkdir -p dist/templates && cp src/templates/*.template dist/templates", 15 | "dev": "concurrently \"npm run demo:dev\" \"npm run docs:dev\"", 16 | "semantic-release": "semantic-release", 17 | "docs:dev": "vitepress dev docs", 18 | "docs:build": "vitepress build docs", 19 | "docs:preview": "vitepress preview docs", 20 | "demo:dev": "npm run dev -w demo", 21 | "demo:build": "npm run build -w demo", 22 | "test": "vitest" 23 | }, 24 | "keywords": [ 25 | "vite", 26 | "svelte", 27 | "vite-plugin", 28 | "custom-elements", 29 | "shadow-dom", 30 | "web-components" 31 | ], 32 | "author": "Felix", 33 | "license": "MIT", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/vidschofelix/vite-plugin-svelte-anywhere.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/vidschofelix/vite-plugin-svelte-anywhere/issues" 40 | }, 41 | "homepage": "https://svelte-anywhere.dev", 42 | "devDependencies": { 43 | "@types/node": "^24.0.10", 44 | "concurrently": "^9.1.2", 45 | "cpx2": "^8.0.0", 46 | "eslint": "^9.33.0", 47 | "memfs": "^4.17.0", 48 | "semantic-release": "^24.2.1", 49 | "svelte": "^5.0.0", 50 | "tsup": "^8.5.0", 51 | "typescript": "^5.9.2", 52 | "vite": "^7.1.1", 53 | "vitepress": "^1.6.3", 54 | "vitepress-plugin-llms": "^1.7.2", 55 | "vitest": "^3.0.4" 56 | }, 57 | "peerDependencies": { 58 | "svelte": "^5.0.0", 59 | "vite": "^6.0.0 || ^7.0.0" 60 | }, 61 | "files": [ 62 | "dist", 63 | "README.md" 64 | ], 65 | "publishConfig": { 66 | "access": "public" 67 | }, 68 | "workspaces": [ 69 | "demo", 70 | "." 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /release.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ["main", {name: "next", prerelease: true}] 3 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'node:fs'; 2 | import * as path from 'path'; 3 | import { Plugin, normalizePath } from 'vite'; 4 | 5 | interface SvelteAnywhereOptions { 6 | componentsDir?: string; 7 | outputDir?: string; 8 | defaultTemplate?: string; 9 | defaultShadowMode?: 'open' | 'none'; 10 | templatesDir?: string; 11 | cleanOutputDir?: boolean; 12 | log?: boolean; 13 | } 14 | 15 | interface ComponentData { 16 | tag: string; 17 | template: string; 18 | shadow: string; 19 | generatedPath: string; 20 | } 21 | 22 | export default function svelteAnywhere(options: SvelteAnywhereOptions = {}): Plugin { 23 | const { 24 | componentsDir = 'src', 25 | outputDir = 'src/generated/custom-element', 26 | defaultTemplate = 'lazy', 27 | defaultShadowMode = 'none', 28 | templatesDir, 29 | cleanOutputDir = true, 30 | log = false, 31 | } = options; 32 | 33 | const customTemplatesDir = templatesDir ? path.resolve(process.cwd(), templatesDir) : null; 34 | const defaultTemplatesDir = path.resolve(import.meta.dirname , './templates'); 35 | 36 | const templateCache = new Map(); 37 | const components = new Map(); 38 | const registeredTags = new Map(); 39 | const outputPath = path.resolve(outputDir); 40 | 41 | const logInfo = (message: string) => log && console.log(`[svelte-anywhere] ${message}`); 42 | const logError = (message: string) => log && console.error(`[svelte-anywhere] ERROR: ${message}`); 43 | 44 | function validateShadowMode(shadow: string): void { 45 | if (!['open', 'none'].includes(shadow)) { 46 | throw new Error(`Invalid shadow mode "${shadow}". Allowed values: "open", "none".`); 47 | } 48 | } 49 | 50 | function validateTagName(tag: string): void { 51 | if (!/^[a-z][a-z0-9\-]*\-[a-z0-9\-]+$/.test(tag)) { 52 | throw new Error(`Invalid tag "${tag}". Must be lowercase with hyphen.`); 53 | } 54 | } 55 | 56 | async function loadTemplate(name: string): Promise { 57 | if (templateCache.has(name)) return templateCache.get(name)!; 58 | 59 | const attemptedPaths: string[] = []; 60 | let content: string | undefined; 61 | 62 | // Try custom templates first 63 | if (customTemplatesDir) { 64 | const customPath = path.join(customTemplatesDir, `${name}.template`); 65 | attemptedPaths.push(customPath); 66 | try { 67 | content = await fs.readFile(customPath, 'utf-8'); 68 | } catch { 69 | logInfo(`Template "${name}" not found in custom directory, using default...`); 70 | } 71 | } 72 | 73 | // Fallback to default templates 74 | if (!content) { 75 | const defaultPath = path.join(defaultTemplatesDir, `${name}.template`); 76 | attemptedPaths.push(defaultPath); 77 | try { 78 | content = await fs.readFile(defaultPath, 'utf-8'); 79 | } catch { 80 | const errorMessage = `Template "${name}" not found in:\n${attemptedPaths.join('\n')}`; 81 | logError(errorMessage); 82 | throw new Error(errorMessage); 83 | } 84 | } 85 | 86 | templateCache.set(name, content); 87 | return content; 88 | } 89 | 90 | async function processComponent(filePath: string): Promise { 91 | if (!filePath.endsWith('.svelte')) return; 92 | 93 | const content = await fs.readFile(filePath, 'utf-8'); 94 | const match = content.match(/@custom-element\s+(\S+)(?:\s+shadow=(\S+))?(?:\s+template=(\S+))?/); 95 | const normalizedPath = normalizePath(filePath); 96 | 97 | const existing = components.get(normalizedPath); 98 | 99 | if (match) { 100 | const [_, tag, shadow = defaultShadowMode, template = defaultTemplate] = match; 101 | validateTagName(tag); 102 | validateShadowMode(shadow); 103 | 104 | // Check for tag conflicts first 105 | if (registeredTags.has(tag) && registeredTags.get(tag) !== normalizedPath) { 106 | logError(`Tag "${tag}" already registered by ${registeredTags.get(tag)}`); 107 | throw new Error(`Duplicate custom-element tag: ${tag}`); 108 | } 109 | 110 | // Check if configuration changed 111 | const templateChanged = existing?.template !== template; 112 | const shadowChanged = existing?.shadow !== shadow; 113 | const tagChanged = existing?.tag !== tag; 114 | 115 | if (existing) { 116 | if (tagChanged) { 117 | await removeGeneratedFile(existing.generatedPath); 118 | registeredTags.delete(existing.tag); 119 | } 120 | } 121 | 122 | const componentData: ComponentData = { 123 | tag, 124 | template: template.trim(), 125 | shadow: shadow.trim(), 126 | generatedPath: path.resolve(outputPath, `${tag}.svelte`) 127 | }; 128 | 129 | components.set(normalizedPath, componentData); 130 | registeredTags.set(tag, normalizedPath); 131 | 132 | if (tagChanged || templateChanged || shadowChanged) { 133 | await generateComponent(normalizedPath); 134 | } else { 135 | logInfo(`Skipping unchanged component: ${tag}`); 136 | } 137 | } else if (existing) { 138 | await removeGeneratedFile(existing.generatedPath); 139 | components.delete(normalizedPath); 140 | registeredTags.delete(existing.tag); 141 | } 142 | } 143 | 144 | async function generateComponent(filePath: string): Promise { 145 | const component = components.get(normalizePath(filePath)); 146 | if (!component) return; 147 | 148 | const { tag, template, shadow, generatedPath } = component; 149 | const templateContent = await loadTemplate(template); 150 | const relativePath = normalizePath(path.relative(outputPath, filePath)); 151 | 152 | const content = templateContent 153 | .replace(/{{CUSTOM_ELEMENT_TAG}}/g, tag) 154 | .replace(/{{SVELTE_PATH}}/g, relativePath) 155 | .replace(/{{SHADOW_MODE}}/g, shadow); 156 | 157 | await fs.mkdir(outputPath, { recursive: true }); 158 | await fs.writeFile(generatedPath, content, 'utf-8'); 159 | logInfo(`Generated: ${generatedPath}`); 160 | } 161 | 162 | async function removeGeneratedFile(generatedPath: string): Promise { 163 | try { 164 | await fs.rm(generatedPath, { force: true }); 165 | logInfo(`Removed: ${generatedPath}`); 166 | } catch (error) { 167 | logError(`Failed to remove ${generatedPath}: ${error}`); 168 | } 169 | } 170 | 171 | async function collectComponents(dir: string): Promise { 172 | const entries = await fs.readdir(normalizePath(dir), { withFileTypes: true }); 173 | await Promise.all(entries.map(async (entry) => { 174 | const fullPath = path.join(dir, entry.name); 175 | entry.isDirectory() ? await collectComponents(fullPath) : await processComponent(fullPath); 176 | })); 177 | } 178 | 179 | return { 180 | name: 'vite-plugin-svelte-anywhere', 181 | 182 | async buildStart() { 183 | logInfo('Initializing plugin...'); 184 | await loadTemplate(defaultTemplate); 185 | 186 | if (cleanOutputDir) { 187 | await fs.rm(outputPath, { recursive: true, force: true }); 188 | logInfo('Cleaned output directory'); 189 | } 190 | 191 | await collectComponents(path.resolve(componentsDir)); 192 | }, 193 | 194 | configureServer(server) { 195 | server.watcher 196 | .on('change', async (file) => { 197 | if (file.endsWith('.svelte')) { 198 | logInfo(`Detected change: ${file}`); 199 | await processComponent(file); 200 | } 201 | }) 202 | .on('unlink', async (file) => { 203 | if (file.endsWith('.svelte')) { 204 | logInfo(`Detected removal: ${file}`); 205 | const component = components.get(normalizePath(file)); 206 | if (component) { 207 | await removeGeneratedFile(component.generatedPath); 208 | components.delete(normalizePath(file)); 209 | registeredTags.delete(component.tag); 210 | } 211 | } 212 | }); 213 | } 214 | }; 215 | } -------------------------------------------------------------------------------- /src/templates/eager.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /src/templates/lazy.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | {#await import('{{SVELTE_PATH}}') then { default: Component }} 9 | 10 | {/await} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules","dist"] 9 | } --------------------------------------------------------------------------------