@vuecast/astro-module 1.0.2 → 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 +50 -43
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.mjs +173 -21
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,86 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@vuecast/astro-module` (for Vue-SFC lovers)
|
|
2
2
|
|
|
3
|
-
Write Vue
|
|
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 `.
|
|
8
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 `.
|
|
43
|
+
2. Create `.astro` files in your `src/pages` directory and use Vue-template syntax below the frontmatter:
|
|
54
44
|
|
|
55
|
-
```
|
|
56
|
-
<!-- src/pages/index.
|
|
57
|
-
|
|
45
|
+
```astro
|
|
46
|
+
<!-- src/pages/index.astro -->
|
|
47
|
+
---
|
|
48
|
+
const isLoggedIn = true;
|
|
58
49
|
const fruits = ["apples", "oranges", "bananas", "cherries", "grapes"];
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
82
|
-
2.
|
|
83
|
-
3.
|
|
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,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HookParameters } from 'astro';
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
};
|
|
4
10
|
|
|
5
|
-
export {
|
|
11
|
+
export { VuecastAstro, VuecastAstro as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HookParameters } from 'astro';
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
};
|
|
4
10
|
|
|
5
|
-
export {
|
|
11
|
+
export { VuecastAstro, VuecastAstro as default };
|
package/dist/index.mjs
CHANGED
|
@@ -1,27 +1,179 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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, "&").replace(/"/g, """);
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
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() {
|
|
|
29
181
|
};
|
|
30
182
|
}
|
|
31
183
|
|
|
32
|
-
export {
|
|
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.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Astro integration for Vue
|
|
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
|
}
|