├── .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 | 
2 |
3 | # Vite Plugin: Svelte Anywhere
4 |
5 | [](https://www.npmjs.com/package/vite-plugin-svelte-anywhere)
6 | [](https://img.shields.io/npm/d18m/vite-plugin-svelte-anywhere)
7 | [](https://github.com/vidschofelix/vite-plugin-svelte-anywhere/actions)
8 | [](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 | count++}>
45 | Clicked {count} times
46 |
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 |
12 | count is {count}
13 |
14 |
--------------------------------------------------------------------------------
/demo/src/lib/PokemonWidget.svelte:
--------------------------------------------------------------------------------
1 |
2 |
24 |
25 |
26 |
27 | {#key pokemon?.name}
28 |
32 | {/key}
33 |
34 |
35 |
36 | {#key pokemon?.name}
37 |
{pokemon?.name ?? "Loading"}
39 | {/key}
40 |
41 |
42 |
45 | Get Random Pokémon
46 |
47 |
--------------------------------------------------------------------------------
/demo/src/lib/ShadowCounter.svelte:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 | count is {count}
13 |
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 |
15 |
16 |
17 |
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 | }
--------------------------------------------------------------------------------