@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.
Files changed (4) hide show
  1. package/README.md +28 -8
  2. package/index.d.ts +37 -6
  3. package/index.js +72 -101
  4. 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
- - Auto-generates `App.svelte`, `Link.svelte`, `[...path].astro`, `page.svelte`, and `404.svelte`
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 { astroSvelteSpa } from "@volpe/astro-svelte-spa"
27
+ import { plugin } from "@volpe/astro-svelte-spa"
27
28
 
28
29
  export default defineConfig({
29
30
  integrations: [svelte()],
30
31
  vite: {
31
- plugins: [astroSvelteSpa({ basePath: "/app" })]
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 AstroSvelteSpaOptions {
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 `App.svelte`, `Link.svelte`, `[...path].astro`, `page.svelte`, and `404.svelte`
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 { astroSvelteSpa } from "@volpe/astro-svelte-spa"
27
+ * import { plugin } from "@volpe/astro-svelte-spa"
27
28
  *
28
29
  * export default defineConfig({
29
30
  * vite: {
30
- * plugins: [astroSvelteSpa({ basePath: "/app" })]
31
+ * plugins: [plugin({ basePath: "/app" })]
31
32
  * }
32
33
  * })
33
34
  * ```
34
35
  */
35
- export function astroSvelteSpa(options?: AstroSvelteSpaOptions): Plugin;
36
+ export function plugin(options?: PluginOptions): Plugin;
36
37
 
37
- export default astroSvelteSpa;
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 generateComponentFiles(rootDir, pagesDir, basePath) {
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 "~/components/App.svelte"
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 astroSvelteSpa(options = {}) {
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
- generateComponentFiles(rootDir, pagesDir, basePath);
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 astroSvelteSpa;
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.0",
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"