@vuecast/astro-module 1.0.1 → 1.0.4

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 CHANGED
@@ -1,86 +1,93 @@
1
- # @vuecast/astro-module
1
+ # `@vuecast/astro-module` (for Vue-SFC lovers)
2
2
 
3
- Write Vue Single File Components (`.vue` files) in your Astro projects.
3
+ Write Vue-template syntax inside `.astro` files in your Astro projects instead of Astro's JSX syntax.
4
4
 
5
5
  ## Features
6
6
 
7
- - Use `.vue` files as pages in your Astro project
8
- - Full Vue SFC support with all Vue features
7
+ - Use Vue-template syntax inside `.astro` files
8
+ - Supports common Vue template features like `v-if`, `v-for`, `:bind`, and `{{ ... }}`
9
9
  - Seamless integration with Astro's build system
10
- - Ensures proper head rendering support
10
+ - Keeps Astro frontmatter intact
11
11
 
12
12
  ## Prerequisites
13
13
 
14
14
  First, scaffold a new Astro project:
15
15
 
16
16
  ```bash
17
- npm create astro@latest
17
+ npm create astro@latest # or pnpm, bun
18
18
  ```
19
19
 
20
- Install the official Astro Vue integration:
21
-
22
- ```bash
23
- npx astro add vue
24
- ```
20
+ You do not need the Astro Vue integration unless you plan to use Vue components or `.vue` files in your project. See the [Astro Vue integration guide](https://docs.astro.build/en/guides/integrations-guide/vue/).
25
21
 
26
22
  ## Installation
27
23
 
28
24
  ```bash
29
- pnpm add @vuecast/astro-module
30
- # or
31
25
  npm install @vuecast/astro-module
32
- # or
33
- yarn add @vuecast/astro-module
26
+ # or pnpm add @vuecast/astro-module
27
+ # or bun add @vuecast/astro-module
34
28
  ```
35
29
 
36
30
  ## Usage
37
31
 
38
- 1. Add the integration to your `astro.config.mjs`. Note that `@vuecast/astro-module` must be added after the Vue integration:
32
+ 1. Add the integration to your `astro.config.mjs`:
39
33
 
40
34
  ```js
41
35
  import { defineConfig } from "astro/config";
42
- import vue from "@astrojs/vue";
43
36
  import vuecast from "@vuecast/astro-module";
44
37
 
45
38
  export default defineConfig({
46
- integrations: [
47
- vue(), // Vue integration must come first
48
- vuecast(), // Then add VueCast
49
- ],
39
+ integrations: [vuecast()],
50
40
  });
51
41
  ```
52
42
 
53
- 2. Create `.vue` files in your `src/pages` directory:
43
+ 2. Create `.astro` files in your `src/pages` directory and use Vue-template syntax below the frontmatter:
54
44
 
55
- ```vue
56
- <!-- src/pages/index.vue -->
57
- <script setup lang="ts">
45
+ ```astro
46
+ <!-- src/pages/index.astro -->
47
+ ---
48
+ const isLoggedIn = true;
58
49
  const fruits = ["apples", "oranges", "bananas", "cherries", "grapes"];
59
- </script>
60
-
61
- <template>
62
- <div>
63
- <h1>Hello from VueCast!</h1>
64
- <ul>
65
- <li v-for="(fruit, index) in fruits" :key="index">
66
- {{ index + 1 }}: {{ fruit }}
67
- </li>
68
- </ul>
69
- </div>
70
- </template>
50
+ ---
51
+
52
+ <div>
53
+ <p v-if="isLoggedIn">Welcome back!</p>
54
+ <h1>Hello from VueCast!</h1>
55
+ <ul>
56
+ <li v-for="(fruit, index) in fruits" :key="index" :data-name="fruit">
57
+ {{ index + 1 }}: {{ fruit }}
58
+ </li>
59
+ </ul>
60
+ </div>
61
+ ```
71
62
 
72
- <style scoped>
73
- /* Your component styles here */
74
- </style>
63
+ Transformed output (Astro JSX-style):
64
+
65
+ ```astro
66
+ <div>
67
+ {isLoggedIn ? (<p>Welcome back!</p>) : null}
68
+ <h1>Hello from VueCast!</h1>
69
+ <ul>
70
+ {fruits.map((fruit, index) => (
71
+ <li key={index} data-name={fruit}>
72
+ {index + 1}: {fruit}
73
+ </li>
74
+ ))}
75
+ </ul>
76
+ </div>
75
77
  ```
76
78
 
79
+ Notes:
80
+
81
+ - Supported directives: `v-if`, `v-for`, `:bind`, and `{{ ... }}` interpolation.
82
+ - `@click` (and other event handlers) are not executed in plain Astro HTML. They only work inside hydrated islands; the transform preserves them as data attributes (e.g. `data-on-click`) for later use.
83
+
77
84
  ## How it Works
78
85
 
79
86
  The integration:
80
87
 
81
- 1. Registers `.vue` as a valid page extension in Astro
82
- 2. Sets up the Vue renderer for processing `.vue` files
83
- 3. Ensures proper head rendering support for Vue components
88
+ 1. Detects Vue-template syntax inside `.astro` files
89
+ 2. Converts Vue template syntax to Astro/JSX-compatible syntax
90
+ 3. Keeps frontmatter unchanged
84
91
  4. Integrates with Astro's build system through Vite
85
92
 
86
93
  ## License
package/dist/index.d.mts CHANGED
@@ -1,8 +1,11 @@
1
- import { AstroIntegration } from 'astro';
1
+ import { HookParameters } from 'astro';
2
2
 
3
- interface VuecastAstroPluginOptions {
4
- }
3
+ type SetupHookParams = HookParameters<"astro:config:setup">;
4
+ declare function VuecastAstro(): {
5
+ name: string;
6
+ hooks: {
7
+ "astro:config:setup": (params: SetupHookParams) => Promise<void>;
8
+ };
9
+ };
5
10
 
6
- declare function vuecastAstroIntegration(options: VuecastAstroPluginOptions): AstroIntegration;
7
-
8
- export { vuecastAstroIntegration as VuecastAstro, vuecastAstroIntegration as default };
11
+ export { VuecastAstro, VuecastAstro as default };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
- import { AstroIntegration } from 'astro';
1
+ import { HookParameters } from 'astro';
2
2
 
3
- interface VuecastAstroPluginOptions {
4
- }
3
+ type SetupHookParams = HookParameters<"astro:config:setup">;
4
+ declare function VuecastAstro(): {
5
+ name: string;
6
+ hooks: {
7
+ "astro:config:setup": (params: SetupHookParams) => Promise<void>;
8
+ };
9
+ };
5
10
 
6
- declare function vuecastAstroIntegration(options: VuecastAstroPluginOptions): AstroIntegration;
7
-
8
- export { vuecastAstroIntegration as VuecastAstro, vuecastAstroIntegration as default };
11
+ export { VuecastAstro, VuecastAstro as default };
package/dist/index.mjs CHANGED
@@ -1,27 +1,179 @@
1
- function vuecastAstroIntegration(options) {
1
+ import { baseParse, NodeTypes } from '@vue/compiler-dom';
2
+
3
+ function vueTemplateAstroTransform(code, id = "Component.astro") {
4
+ const split = splitAstroFrontmatter(code);
5
+ if (!split)
6
+ return null;
7
+ const { frontmatterBlock, templatePart } = split;
8
+ if (!isVueTemplate(templatePart))
9
+ return null;
10
+ const vueAst = baseParse(templatePart, { comments: true });
11
+ const astroTemplate = genChildren(vueAst.children).trimEnd();
2
12
  return {
3
- name: "@vuecast/astro",
13
+ code: `${frontmatterBlock}
14
+ ${astroTemplate}
15
+ `
16
+ };
17
+ }
18
+ function splitAstroFrontmatter(code) {
19
+ if (!code.startsWith("---"))
20
+ return null;
21
+ const fence = "\n---";
22
+ const end = code.indexOf(fence, 3);
23
+ if (end === -1)
24
+ return null;
25
+ const frontmatterBlock = code.slice(0, end + fence.length);
26
+ const templatePart = code.slice(end + fence.length).replace(/^\r?\n/, "");
27
+ return { frontmatterBlock, templatePart };
28
+ }
29
+ function isVueTemplate(template) {
30
+ if (/\{\{\s*[^}]+?\s*\}\}/.test(template))
31
+ return true;
32
+ if (/\sv-(if|else-if|else|for|show|model|bind|on|slot)\b/.test(template))
33
+ return true;
34
+ if (/(?:\s|<)[:@][a-zA-Z]/.test(template))
35
+ return true;
36
+ return false;
37
+ }
38
+ function genChildren(children) {
39
+ return children.map(genNode).join("");
40
+ }
41
+ function genNode(node) {
42
+ switch (node.type) {
43
+ case NodeTypes.TEXT:
44
+ return node.content;
45
+ case NodeTypes.INTERPOLATION:
46
+ return `{${node.content.content}}`;
47
+ case NodeTypes.ELEMENT:
48
+ return genElement(node);
49
+ case NodeTypes.COMMENT:
50
+ return `<!--${node.content}-->`;
51
+ default:
52
+ return "";
53
+ }
54
+ }
55
+ function genElement(el) {
56
+ const tag = el.tag;
57
+ const { attrs, vIf, vElseIf, vElse, vFor } = splitDirectives(el.props);
58
+ if (vFor) {
59
+ const { source, value, index } = parseVFor(vFor.exp?.content ?? "");
60
+ const inner = genPlainElement(tag, attrs, el.children);
61
+ const args = index ? `${value}, ${index}` : value;
62
+ return `{${source}.map((${args}) => (${inner}))}`;
63
+ }
64
+ if (vIf) {
65
+ const cond = vIf.exp?.content ?? "false";
66
+ const thenBranch = genPlainElement(tag, attrs, el.children);
67
+ return `{${cond} ? (${thenBranch}) : null}`;
68
+ }
69
+ if (vElseIf || vElse) {
70
+ return genPlainElement(tag, attrs, el.children);
71
+ }
72
+ return genPlainElement(tag, attrs, el.children);
73
+ }
74
+ function genPlainElement(tag, props, children) {
75
+ const attrStr = props.map(genAttr).join("");
76
+ const inner = genChildren(children);
77
+ return `<${tag}${attrStr}>${inner}</${tag}>`;
78
+ }
79
+ function genAttr(p) {
80
+ if (p.type === NodeTypes.ATTRIBUTE) {
81
+ if (!p.value)
82
+ return ` ${p.name}`;
83
+ return ` ${p.name}="${escapeAttr(p.value.content)}"`;
84
+ }
85
+ if (p.type === NodeTypes.DIRECTIVE && p.name === "bind") {
86
+ const name = p.arg?.content;
87
+ if (!name)
88
+ return "";
89
+ const expr = p.exp?.content ?? "true";
90
+ return ` ${name}={${expr}}`;
91
+ }
92
+ if (p.type === NodeTypes.DIRECTIVE && p.name === "on") {
93
+ const evt = p.arg?.content ?? "event";
94
+ const expr = p.exp?.content ?? "";
95
+ return ` data-on-${evt}="${escapeAttr(expr)}"`;
96
+ }
97
+ return "";
98
+ }
99
+ function splitDirectives(props) {
100
+ const attrs = [];
101
+ let vIf = null;
102
+ let vElseIf = null;
103
+ let vElse = null;
104
+ let vFor = null;
105
+ for (const p of props) {
106
+ if (p.type === NodeTypes.DIRECTIVE) {
107
+ if (p.name === "if")
108
+ vIf = p;
109
+ else if (p.name === "else-if")
110
+ vElseIf = p;
111
+ else if (p.name === "else")
112
+ vElse = p;
113
+ else if (p.name === "for")
114
+ vFor = p;
115
+ else
116
+ attrs.push(p);
117
+ } else {
118
+ attrs.push(p);
119
+ }
120
+ }
121
+ return { attrs, vIf, vElseIf, vElse, vFor };
122
+ }
123
+ function parseVFor(exp) {
124
+ const m = exp.match(/^\s*(.+?)\s+in\s+(.+)\s*$/);
125
+ if (!m)
126
+ return { source: exp, value: "item", index: null };
127
+ const lhs = m[1].trim();
128
+ const source = m[2].trim();
129
+ if (lhs.startsWith("(") && lhs.endsWith(")")) {
130
+ const parts = lhs.slice(1, -1).split(",").map((s) => s.trim());
131
+ return {
132
+ source,
133
+ value: parts[0] || "item",
134
+ index: parts[1] || null
135
+ };
136
+ }
137
+ return { source, value: lhs || "item", index: null };
138
+ }
139
+ function escapeAttr(s) {
140
+ return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;");
141
+ }
142
+
143
+ function vueTemplatePreAstroPlugin() {
144
+ return {
145
+ name: "vuecast-vue-template-pre-astro",
146
+ enforce: "pre",
147
+ configResolved(config) {
148
+ const plugins = config.plugins;
149
+ const idx = plugins.findIndex(
150
+ (p) => p && typeof p === "object" && "name" in p && p.name === "vuecast-vue-template-pre-astro"
151
+ );
152
+ if (idx > 0) {
153
+ const [self] = plugins.splice(idx, 1);
154
+ plugins.unshift(self);
155
+ }
156
+ },
157
+ async transform(code, id) {
158
+ const [filepath] = id.split("?");
159
+ if (!filepath.endsWith(".astro"))
160
+ return null;
161
+ const res = vueTemplateAstroTransform(code, filepath);
162
+ return res ? res.code : null;
163
+ }
164
+ };
165
+ }
166
+
167
+ function VuecastAstro() {
168
+ return {
169
+ name: "@vuecast/astro-vue-template",
4
170
  hooks: {
5
171
  "astro:config:setup": async (params) => {
6
- const { updateConfig, addPageExtension, addRenderer } = params;
7
- addRenderer({
8
- name: "vuecast:astro",
9
- serverEntrypoint: "./node_modules/@astrojs/vue/dist/server.js"
10
- });
11
- addPageExtension(".vue");
12
- updateConfig({
172
+ const existingPlugins = params.config.vite?.plugins;
173
+ const normalizedPlugins = Array.isArray(existingPlugins) ? existingPlugins : existingPlugins ? [existingPlugins] : [];
174
+ params.updateConfig({
13
175
  vite: {
14
- plugins: [{
15
- name: "vuecast-post",
16
- async transform(code, id) {
17
- if (!id.endsWith(".vue"))
18
- return;
19
- const regex = /(\]\))(;?\n?)$/;
20
- const replacement = ",[Symbol.for('astro.needsHeadRendering'),true]$1";
21
- code = code.replace(regex, replacement);
22
- return code;
23
- }
24
- }]
176
+ plugins: [vueTemplatePreAstroPlugin(), ...normalizedPlugins]
25
177
  }
26
178
  });
27
179
  }
@@ -29,4 +181,4 @@ function vuecastAstroIntegration(options) {
29
181
  };
30
182
  }
31
183
 
32
- export { vuecastAstroIntegration as VuecastAstro, vuecastAstroIntegration as default };
184
+ export { VuecastAstro, VuecastAstro as default };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vuecast/astro-module",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
- "description": "Astro integration for Vue SFC pages",
5
+ "description": "Astro integration for Vue template syntax in .astro pages",
6
6
  "main": "./dist/index.mjs",
7
7
  "module": "./dist/index.mjs",
8
8
  "types": "./dist/index.d.ts",
@@ -33,5 +33,8 @@
33
33
  "license": "MIT",
34
34
  "devDependencies": {
35
35
  "unbuild": "^2.0.0"
36
+ },
37
+ "dependencies": {
38
+ "@vue/compiler-dom": "^3.5.27"
36
39
  }
37
40
  }