@volpe/astro-svelte-spa 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -8
- package/index.d.ts +37 -6
- package/index.js +72 -101
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -5,9 +5,10 @@ Vite plugin for file-based routing with svelte5-router in Astro.
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- File-based routing with `page.svelte` and `layout.svelte`
|
|
8
|
-
-
|
|
8
|
+
- Pre-built `App.svelte` and `Link.svelte` components
|
|
9
|
+
- Auto-generates `[...path].astro`, `page.svelte`, and `404.svelte`
|
|
9
10
|
- Virtual module `virtual:svelte-routes` with typed routes
|
|
10
|
-
- `SvelteAppRoutes` type for autocomplete
|
|
11
|
+
- `SvelteAppRoutes` type for autocomplete in `<Link>`
|
|
11
12
|
- 404 handling with `StatusCode.NotFound`
|
|
12
13
|
- HMR support with full reload on changes
|
|
13
14
|
|
|
@@ -23,16 +24,38 @@ npm install @volpe/astro-svelte-spa @mateothegreat/svelte5-router
|
|
|
23
24
|
// astro.config.mjs
|
|
24
25
|
import { defineConfig } from "astro/config"
|
|
25
26
|
import svelte from "@astrojs/svelte"
|
|
26
|
-
import {
|
|
27
|
+
import { plugin } from "@volpe/astro-svelte-spa"
|
|
27
28
|
|
|
28
29
|
export default defineConfig({
|
|
29
30
|
integrations: [svelte()],
|
|
30
31
|
vite: {
|
|
31
|
-
plugins: [
|
|
32
|
+
plugins: [plugin({ basePath: "/app" })]
|
|
32
33
|
}
|
|
33
34
|
})
|
|
34
35
|
```
|
|
35
36
|
|
|
37
|
+
## Components
|
|
38
|
+
|
|
39
|
+
Import the pre-built components:
|
|
40
|
+
|
|
41
|
+
```svelte
|
|
42
|
+
<!-- src/pages/svelte-app/[...path].astro -->
|
|
43
|
+
---
|
|
44
|
+
import App from "@volpe/astro-svelte-spa/App.svelte"
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
<App client:only="svelte" />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```svelte
|
|
51
|
+
<!-- In your Svelte pages -->
|
|
52
|
+
<script>
|
|
53
|
+
import Link from "@volpe/astro-svelte-spa/Link.svelte"
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<Link href="/svelte-app/about">About</Link>
|
|
57
|
+
```
|
|
58
|
+
|
|
36
59
|
## File Structure
|
|
37
60
|
|
|
38
61
|
```
|
|
@@ -41,15 +64,12 @@ src/
|
|
|
41
64
|
svelte-app/ # or your custom basePath
|
|
42
65
|
page.svelte # -> /svelte-app
|
|
43
66
|
404.svelte # -> catch-all 404
|
|
44
|
-
[...path].astro # Astro catch-all
|
|
67
|
+
[...path].astro # Astro catch-all (auto-generated)
|
|
45
68
|
about/
|
|
46
69
|
page.svelte # -> /svelte-app/about
|
|
47
70
|
settings/
|
|
48
71
|
page.svelte # -> /svelte-app/settings
|
|
49
72
|
layout.svelte # wraps settings pages
|
|
50
|
-
components/
|
|
51
|
-
App.svelte # auto-generated
|
|
52
|
-
Link.svelte # auto-generated
|
|
53
73
|
```
|
|
54
74
|
|
|
55
75
|
## Options
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Plugin } from "vite";
|
|
2
|
+
import type { Component } from "svelte";
|
|
2
3
|
|
|
3
|
-
export interface
|
|
4
|
+
export interface PluginOptions {
|
|
4
5
|
/**
|
|
5
6
|
* Base path for all routes.
|
|
6
7
|
* When set to "/app", the plugin scans `src/pages/app/` and routes start with "/app".
|
|
@@ -14,7 +15,7 @@ export interface AstroSvelteSpaOptions {
|
|
|
14
15
|
*
|
|
15
16
|
* Features:
|
|
16
17
|
* - File-based routing with `page.svelte` and `layout.svelte`
|
|
17
|
-
* - Auto-generates `
|
|
18
|
+
* - Auto-generates `[...path].astro`, `page.svelte`, and `404.svelte`
|
|
18
19
|
* - Virtual module `virtual:svelte-routes` with typed routes
|
|
19
20
|
* - `SvelteAppRoutes` type for autocomplete
|
|
20
21
|
* - 404 handling with StatusCode.NotFound
|
|
@@ -23,15 +24,45 @@ export interface AstroSvelteSpaOptions {
|
|
|
23
24
|
* @example
|
|
24
25
|
* ```js
|
|
25
26
|
* // astro.config.mjs
|
|
26
|
-
* import {
|
|
27
|
+
* import { plugin } from "@volpe/astro-svelte-spa"
|
|
27
28
|
*
|
|
28
29
|
* export default defineConfig({
|
|
29
30
|
* vite: {
|
|
30
|
-
* plugins: [
|
|
31
|
+
* plugins: [plugin({ basePath: "/app" })]
|
|
31
32
|
* }
|
|
32
33
|
* })
|
|
33
34
|
* ```
|
|
34
35
|
*/
|
|
35
|
-
export function
|
|
36
|
+
export function plugin(options?: PluginOptions): Plugin;
|
|
36
37
|
|
|
37
|
-
export default
|
|
38
|
+
export default plugin;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The main App component that renders the router.
|
|
42
|
+
* Use with `client:only="svelte"` in Astro.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```astro
|
|
46
|
+
* ---
|
|
47
|
+
* import { App } from "@volpe/astro-svelte-spa"
|
|
48
|
+
* ---
|
|
49
|
+
* <App client:only="svelte" />
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export const App: Component<{}>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Link component for client-side navigation.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```svelte
|
|
59
|
+
* <script>
|
|
60
|
+
* import { Link } from "@volpe/astro-svelte-spa"
|
|
61
|
+
* </script>
|
|
62
|
+
* <Link href="/about">About</Link>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export const Link: Component<{
|
|
66
|
+
href: string;
|
|
67
|
+
[key: string]: any;
|
|
68
|
+
}>;
|
package/index.js
CHANGED
|
@@ -4,6 +4,11 @@ import path from "node:path";
|
|
|
4
4
|
const VIRTUAL_MODULE_ID = "virtual:svelte-routes";
|
|
5
5
|
const RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
|
|
6
6
|
|
|
7
|
+
const PACKAGE_ID = "@volpe/astro-svelte-spa";
|
|
8
|
+
const RESOLVED_PACKAGE_ID = "\0" + PACKAGE_ID;
|
|
9
|
+
const VIRTUAL_APP_ID = "\0svelte-spa-app.svelte";
|
|
10
|
+
const VIRTUAL_LINK_ID = "\0svelte-spa-link.svelte";
|
|
11
|
+
|
|
7
12
|
/**
|
|
8
13
|
* @typedef {Object} RouteNode
|
|
9
14
|
* @property {string} path
|
|
@@ -101,15 +106,12 @@ function applyBasePath(routePath, basePath) {
|
|
|
101
106
|
function generateRoutes(node, imports) {
|
|
102
107
|
const routes = [];
|
|
103
108
|
|
|
104
|
-
// Use relative paths (basePath is handled by Router component)
|
|
105
109
|
const routePath = node.path;
|
|
106
110
|
const layoutName = node.layout ? imports.get(node.layout) : null;
|
|
107
111
|
const pageName = node.page ? imports.get(node.page) : null;
|
|
108
112
|
|
|
109
|
-
// Add page route
|
|
110
113
|
if (pageName) {
|
|
111
114
|
if (layoutName) {
|
|
112
|
-
// With layout - use children
|
|
113
115
|
routes.push(`{
|
|
114
116
|
path: "${routePath}",
|
|
115
117
|
component: ${layoutName},
|
|
@@ -118,7 +120,6 @@ function generateRoutes(node, imports) {
|
|
|
118
120
|
]
|
|
119
121
|
}`);
|
|
120
122
|
} else {
|
|
121
|
-
// No layout - simple route with async loading
|
|
122
123
|
routes.push(`{
|
|
123
124
|
path: "${routePath}",
|
|
124
125
|
component: async () => import("${node.page}")
|
|
@@ -126,7 +127,6 @@ function generateRoutes(node, imports) {
|
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
// Add children routes
|
|
130
130
|
for (const child of node.children) {
|
|
131
131
|
routes.push(...generateRoutes(child, imports));
|
|
132
132
|
}
|
|
@@ -144,16 +144,13 @@ function generateRoutesModule(pagesDir, basePath) {
|
|
|
144
144
|
const imports = new Map();
|
|
145
145
|
generateImports(tree, imports);
|
|
146
146
|
|
|
147
|
-
// Import layouts (pages are loaded async)
|
|
148
147
|
const layoutImports = Array.from(imports.entries())
|
|
149
148
|
.filter(([filePath]) => filePath.includes("layout.svelte"))
|
|
150
149
|
.map(([filePath, name]) => `import ${name} from "${filePath}"`)
|
|
151
150
|
.join("\n");
|
|
152
151
|
|
|
153
|
-
// Routes use relative paths - basePath is handled by Router component
|
|
154
152
|
const routes = generateRoutes(tree, imports);
|
|
155
153
|
|
|
156
|
-
// Check for 404.svelte for statuses
|
|
157
154
|
const notFoundPath = path.join(pagesDir, "404.svelte");
|
|
158
155
|
const hasNotFound = fs.existsSync(notFoundPath);
|
|
159
156
|
const notFoundImportStatement = hasNotFound
|
|
@@ -215,101 +212,17 @@ declare module "virtual:svelte-routes" {
|
|
|
215
212
|
}
|
|
216
213
|
|
|
217
214
|
/**
|
|
218
|
-
* @param {string} rootDir
|
|
219
215
|
* @param {string} pagesDir
|
|
220
|
-
* @param {string} basePath
|
|
221
216
|
*/
|
|
222
|
-
function
|
|
223
|
-
const componentsDir = path.resolve(rootDir, "src/components");
|
|
224
|
-
|
|
225
|
-
if (!fs.existsSync(componentsDir)) {
|
|
226
|
-
fs.mkdirSync(componentsDir, { recursive: true });
|
|
227
|
-
}
|
|
228
|
-
|
|
217
|
+
function generatePageFiles(pagesDir) {
|
|
229
218
|
if (!fs.existsSync(pagesDir)) {
|
|
230
219
|
fs.mkdirSync(pagesDir, { recursive: true });
|
|
231
220
|
}
|
|
232
221
|
|
|
233
|
-
// Generate App.svelte if it doesn't exist
|
|
234
|
-
const appPath = path.resolve(componentsDir, "App.svelte");
|
|
235
|
-
if (!fs.existsSync(appPath)) {
|
|
236
|
-
const appContent = `<script>
|
|
237
|
-
import { Router, route, StatusCode } from "@mateothegreat/svelte5-router"
|
|
238
|
-
import routes, { basePath, notFoundComponent } from "virtual:svelte-routes"
|
|
239
|
-
|
|
240
|
-
// Build statuses config for 404 handling
|
|
241
|
-
const statuses = notFoundComponent ? {
|
|
242
|
-
[StatusCode.NotFound]: { component: notFoundComponent }
|
|
243
|
-
} : undefined
|
|
244
|
-
|
|
245
|
-
// Action that combines route + prefetch on hover
|
|
246
|
-
function link(node) {
|
|
247
|
-
const href = node.getAttribute("href")
|
|
248
|
-
|
|
249
|
-
function preload() {
|
|
250
|
-
const r = routes.find(r => r.path === href)
|
|
251
|
-
if (r?.component && typeof r.component === "function") {
|
|
252
|
-
r.component()
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
node.addEventListener("mouseenter", preload)
|
|
257
|
-
node.addEventListener("touchstart", preload, { passive: true })
|
|
258
|
-
|
|
259
|
-
const routeAction = route(node)
|
|
260
|
-
|
|
261
|
-
return {
|
|
262
|
-
destroy() {
|
|
263
|
-
node.removeEventListener("mouseenter", preload)
|
|
264
|
-
node.removeEventListener("touchstart", preload)
|
|
265
|
-
routeAction?.destroy?.()
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export { link }
|
|
271
|
-
</script>
|
|
272
|
-
|
|
273
|
-
<Router {routes} {basePath} {statuses} />
|
|
274
|
-
`;
|
|
275
|
-
fs.writeFileSync(appPath, appContent);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Generate Link.svelte if it doesn't exist
|
|
279
|
-
const linkPath = path.resolve(componentsDir, "Link.svelte");
|
|
280
|
-
if (!fs.existsSync(linkPath)) {
|
|
281
|
-
const linkContent = `<script>
|
|
282
|
-
import { route } from "@mateothegreat/svelte5-router"
|
|
283
|
-
import routes from "virtual:svelte-routes"
|
|
284
|
-
|
|
285
|
-
let { href, children, ...rest } = $props()
|
|
286
|
-
|
|
287
|
-
function preload() {
|
|
288
|
-
const r = routes.find(r => r.path === href)
|
|
289
|
-
if (r?.component && typeof r.component === "function") {
|
|
290
|
-
r.component()
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
</script>
|
|
294
|
-
|
|
295
|
-
<a
|
|
296
|
-
{href}
|
|
297
|
-
use:route
|
|
298
|
-
onmouseenter={preload}
|
|
299
|
-
ontouchstart={preload}
|
|
300
|
-
{...rest}
|
|
301
|
-
>
|
|
302
|
-
{@render children()}
|
|
303
|
-
</a>
|
|
304
|
-
`;
|
|
305
|
-
fs.writeFileSync(linkPath, linkContent);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Generate [...path].astro if it doesn't exist
|
|
309
222
|
const catchAllPath = path.resolve(pagesDir, "[...path].astro");
|
|
310
223
|
if (!fs.existsSync(catchAllPath)) {
|
|
311
224
|
const catchAllContent = `---
|
|
312
|
-
import App from "
|
|
225
|
+
import { App } from "@volpe/astro-svelte-spa"
|
|
313
226
|
---
|
|
314
227
|
|
|
315
228
|
<App client:only="svelte" />
|
|
@@ -317,7 +230,6 @@ import App from "~/components/App.svelte"
|
|
|
317
230
|
fs.writeFileSync(catchAllPath, catchAllContent);
|
|
318
231
|
}
|
|
319
232
|
|
|
320
|
-
// Generate page.svelte if it doesn't exist
|
|
321
233
|
const pagePath = path.resolve(pagesDir, "page.svelte");
|
|
322
234
|
if (!fs.existsSync(pagePath)) {
|
|
323
235
|
const pageContent = `<h1>Edit me :)</h1>
|
|
@@ -325,7 +237,6 @@ import App from "~/components/App.svelte"
|
|
|
325
237
|
fs.writeFileSync(pagePath, pageContent);
|
|
326
238
|
}
|
|
327
239
|
|
|
328
|
-
// Generate 404.svelte if it doesn't exist
|
|
329
240
|
const notFoundPagePath = path.resolve(pagesDir, "404.svelte");
|
|
330
241
|
if (!fs.existsSync(notFoundPagePath)) {
|
|
331
242
|
const notFoundContent = `<script>
|
|
@@ -366,23 +277,61 @@ import App from "~/components/App.svelte"
|
|
|
366
277
|
}
|
|
367
278
|
}
|
|
368
279
|
|
|
280
|
+
/**
|
|
281
|
+
* Generates the virtual App component code
|
|
282
|
+
* @returns {string}
|
|
283
|
+
*/
|
|
284
|
+
function generateAppComponent() {
|
|
285
|
+
return `<script>
|
|
286
|
+
import { Router, StatusCode } from "@mateothegreat/svelte5-router"
|
|
287
|
+
import routes, { basePath, notFoundComponent } from "virtual:svelte-routes"
|
|
288
|
+
|
|
289
|
+
const statuses = notFoundComponent ? {
|
|
290
|
+
[StatusCode.NotFound]: { component: notFoundComponent }
|
|
291
|
+
} : undefined
|
|
292
|
+
</script>
|
|
293
|
+
|
|
294
|
+
<Router {routes} {basePath} {statuses} />
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Generates the virtual Link component code
|
|
300
|
+
* @returns {string}
|
|
301
|
+
*/
|
|
302
|
+
function generateLinkComponent() {
|
|
303
|
+
return `<script>
|
|
304
|
+
import { route, goto } from "@mateothegreat/svelte5-router"
|
|
305
|
+
|
|
306
|
+
let { href, children, ...rest } = $props()
|
|
307
|
+
|
|
308
|
+
const handleClick = (e) => {
|
|
309
|
+
e.preventDefault()
|
|
310
|
+
goto(href)
|
|
311
|
+
}
|
|
312
|
+
</script>
|
|
313
|
+
|
|
314
|
+
<a {href} onclick={handleClick} class:active={$route.path === href} {...rest}>
|
|
315
|
+
{@render children?.()}
|
|
316
|
+
</a>
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
|
|
369
320
|
/**
|
|
370
321
|
* Vite plugin for file-based routing with svelte5-router in Astro
|
|
371
322
|
* @param {Object} options - Plugin options
|
|
372
323
|
* @param {string} [options.basePath] - Base path for all routes (defaults to "/svelte-app")
|
|
373
324
|
* @returns {import('vite').Plugin}
|
|
374
325
|
*/
|
|
375
|
-
export function
|
|
376
|
-
// Normalize basePath, defaults to "/svelte-app"
|
|
326
|
+
export function plugin(options = {}) {
|
|
377
327
|
const basePath = options.basePath?.replace(/\/$/, "") || "/svelte-app";
|
|
378
|
-
// Directory suffix: "/svelte-app" -> "svelte-app"
|
|
379
328
|
const dirSuffix = basePath.replace(/^\//, "");
|
|
380
329
|
|
|
381
330
|
const rootDir = process.cwd();
|
|
382
331
|
const pagesDir = path.resolve(rootDir, "src/pages", dirSuffix);
|
|
383
332
|
const typesPath = path.resolve(rootDir, "src/svelte-routes.d.ts");
|
|
384
333
|
|
|
385
|
-
|
|
334
|
+
generatePageFiles(pagesDir);
|
|
386
335
|
|
|
387
336
|
return {
|
|
388
337
|
name: "vite-plugin-astro-svelte-spa",
|
|
@@ -427,12 +376,34 @@ export function astroSvelteSpa(options = {}) {
|
|
|
427
376
|
if (id === VIRTUAL_MODULE_ID) {
|
|
428
377
|
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
429
378
|
}
|
|
379
|
+
// Intercept main package import (but not /plugin subpath)
|
|
380
|
+
if (id === PACKAGE_ID) {
|
|
381
|
+
return RESOLVED_PACKAGE_ID;
|
|
382
|
+
}
|
|
383
|
+
// Handle virtual component IDs
|
|
384
|
+
if (id === VIRTUAL_APP_ID || id === VIRTUAL_LINK_ID) {
|
|
385
|
+
return id;
|
|
386
|
+
}
|
|
430
387
|
},
|
|
431
388
|
|
|
432
389
|
load(id) {
|
|
433
390
|
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
434
391
|
return generateRoutesModule(pagesDir, basePath);
|
|
435
392
|
}
|
|
393
|
+
// Virtual package module - exports App, Link, and re-exports plugin
|
|
394
|
+
if (id === RESOLVED_PACKAGE_ID) {
|
|
395
|
+
return `
|
|
396
|
+
export { default as App } from "${VIRTUAL_APP_ID}";
|
|
397
|
+
export { default as Link } from "${VIRTUAL_LINK_ID}";
|
|
398
|
+
export { plugin, default } from "@volpe/astro-svelte-spa/plugin";
|
|
399
|
+
`;
|
|
400
|
+
}
|
|
401
|
+
if (id === VIRTUAL_APP_ID) {
|
|
402
|
+
return generateAppComponent();
|
|
403
|
+
}
|
|
404
|
+
if (id === VIRTUAL_LINK_ID) {
|
|
405
|
+
return generateLinkComponent();
|
|
406
|
+
}
|
|
436
407
|
},
|
|
437
408
|
|
|
438
409
|
handleHotUpdate() {
|
|
@@ -441,4 +412,4 @@ export function astroSvelteSpa(options = {}) {
|
|
|
441
412
|
};
|
|
442
413
|
}
|
|
443
414
|
|
|
444
|
-
export default
|
|
415
|
+
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volpe/astro-svelte-spa",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Vite plugin for file-based routing with svelte5-router in Astro",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,8 +9,16 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./index.d.ts",
|
|
11
11
|
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./plugin": {
|
|
14
|
+
"types": "./index.d.ts",
|
|
15
|
+
"default": "./index.js"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"index.d.ts"
|
|
21
|
+
],
|
|
14
22
|
"keywords": [
|
|
15
23
|
"vite",
|
|
16
24
|
"vite-plugin",
|
|
@@ -23,7 +31,8 @@
|
|
|
23
31
|
"author": "",
|
|
24
32
|
"license": "MIT",
|
|
25
33
|
"peerDependencies": {
|
|
26
|
-
"vite": "^5.0.0 || ^6.0.0"
|
|
34
|
+
"vite": "^5.0.0 || ^6.0.0",
|
|
35
|
+
"@mateothegreat/svelte5-router": "^2.0.0"
|
|
27
36
|
},
|
|
28
37
|
"publishConfig": {
|
|
29
38
|
"access": "public"
|