@tenjot/fumi 0.1.0
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/LICENSE +48 -0
- package/dist/app-DvBaD5aw.mjs +19 -0
- package/dist/client.d.mts +8 -0
- package/dist/client.mjs +15 -0
- package/dist/config-BqdJHMUf.mjs +20 -0
- package/dist/data-CRbUH9pt.mjs +14 -0
- package/dist/index.d.mts +14 -0
- package/dist/index.mjs +437 -0
- package/dist/main.d.mts +1 -0
- package/dist/main.mjs +67 -0
- package/dist/router-BEb_gb9D.d.mts +85 -0
- package/dist/router-DZgDu6-U.mjs +69 -0
- package/dist/ssr.d.mts +8 -0
- package/dist/ssr.mjs +14 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 tenjo-t
|
|
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
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Portions of this software are derived from VitePress
|
|
26
|
+
(https://github.com/vuejs/vitepress).
|
|
27
|
+
|
|
28
|
+
MIT License
|
|
29
|
+
|
|
30
|
+
Copyright (c) 2019-present, Yuxi (Evan You) and VitePress contributors
|
|
31
|
+
|
|
32
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
33
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
34
|
+
in the Software without restriction, including without limitation the rights
|
|
35
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
36
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
37
|
+
furnished to do so, subject to the following conditions:
|
|
38
|
+
|
|
39
|
+
The above copyright notice and this permission notice shall be included in all
|
|
40
|
+
copies or substantial portions of the Software.
|
|
41
|
+
|
|
42
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
43
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
44
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
45
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
46
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
47
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
48
|
+
SOFTWARE.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { n as createRouter, t as RouterSymbol } from "./router-DZgDu6-U.mjs";
|
|
2
|
+
import { n as initData, t as DataSymbol } from "./data-CRbUH9pt.mjs";
|
|
3
|
+
import { createSSRApp } from "vue";
|
|
4
|
+
import App from "@app";
|
|
5
|
+
//#region src/client/app.ts
|
|
6
|
+
/** 初期ルートを元にVue appを作成する */
|
|
7
|
+
function createApp(initialRoute) {
|
|
8
|
+
const app = createSSRApp(App);
|
|
9
|
+
const router = createRouter(initialRoute);
|
|
10
|
+
app.provide(RouterSymbol, router);
|
|
11
|
+
const data = initData(router.route);
|
|
12
|
+
app.provide(DataSymbol, data);
|
|
13
|
+
return {
|
|
14
|
+
app,
|
|
15
|
+
router
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { createApp as t };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { a as useData, n as useRouter, t as useRoute } from "./router-BEb_gb9D.mjs";
|
|
2
|
+
import * as _$vue from "vue";
|
|
3
|
+
|
|
4
|
+
//#region src/client/content.vue.d.ts
|
|
5
|
+
declare const __VLS_export: _$vue.DefineComponent<{}, {}, {}, {}, {}, _$vue.ComponentOptionsMixin, _$vue.ComponentOptionsMixin, {}, string, _$vue.PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, _$vue.ComponentProvideOptions, true, {}, any>;
|
|
6
|
+
declare const _default: typeof __VLS_export;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { _default as Content, useData, useRoute, useRouter };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { a as useRoute, o as useRouter } from "./router-DZgDu6-U.mjs";
|
|
2
|
+
import { r as useData } from "./data-CRbUH9pt.mjs";
|
|
3
|
+
import { createBlock, createCommentVNode, defineComponent, openBlock, resolveDynamicComponent, unref } from "vue";
|
|
4
|
+
//#region src/client/content.vue
|
|
5
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
6
|
+
__name: "content",
|
|
7
|
+
setup(__props) {
|
|
8
|
+
const route = useRoute();
|
|
9
|
+
return (_ctx, _cache) => {
|
|
10
|
+
return unref(route).component ? (openBlock(), createBlock(resolveDynamicComponent(unref(route).component), { key: 0 })) : createCommentVNode("v-if", true);
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { _sfc_main as Content, useData, useRoute, useRouter };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/client/config.ts
|
|
2
|
+
function resolveTitle(config) {
|
|
3
|
+
return config.title ? config.titleTemplate?.replaceAll(":title", config.title) ?? config.title : config.titleTemplate;
|
|
4
|
+
}
|
|
5
|
+
function configToHeadConfig(config) {
|
|
6
|
+
const head = config.head ?? [];
|
|
7
|
+
const title = resolveTitle(config);
|
|
8
|
+
if (title) head.push([
|
|
9
|
+
"title",
|
|
10
|
+
{},
|
|
11
|
+
title
|
|
12
|
+
]);
|
|
13
|
+
if (config.description) head.push(["meta", {
|
|
14
|
+
name: "description",
|
|
15
|
+
content: config.description
|
|
16
|
+
}]);
|
|
17
|
+
return head;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { resolveTitle as n, configToHeadConfig as t };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { computed, inject } from "vue";
|
|
2
|
+
//#region src/client/data.ts
|
|
3
|
+
const DataSymbol = Symbol();
|
|
4
|
+
function initData(route) {
|
|
5
|
+
return { page: computed(() => route.data) };
|
|
6
|
+
}
|
|
7
|
+
/** サイトのメタデータ */
|
|
8
|
+
function useData() {
|
|
9
|
+
const data = inject(DataSymbol);
|
|
10
|
+
if (!data) throw new Error("fumi data not properly injected in app");
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { initData as n, useData as r, DataSymbol as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { c as defineConfig, i as SiteData, o as FumiUserConfig, r as PageData, s as HeadConfig } from "./router-BEb_gb9D.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/content-loader.d.ts
|
|
4
|
+
type DataLoader<T> = {
|
|
5
|
+
watch?: never;
|
|
6
|
+
load(): Promise<T>;
|
|
7
|
+
} | {
|
|
8
|
+
watch: string[];
|
|
9
|
+
load(watchFiles: string[], loadMarkdown: (path: string) => Promise<PageData>): Promise<T>;
|
|
10
|
+
};
|
|
11
|
+
type LoadedData<T extends DataLoader<unknown>> = T extends DataLoader<infer U> ? U : never;
|
|
12
|
+
declare function defineLoader<T>(dataLoader: DataLoader<T>): DataLoader<T>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { type DataLoader, type FumiUserConfig, type HeadConfig, type LoadedData, type PageData, type SiteData, defineConfig, defineLoader };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { i as pathToPageComponentPath } from "./router-DZgDu6-U.mjs";
|
|
2
|
+
import { t as configToHeadConfig } from "./config-BqdJHMUf.mjs";
|
|
3
|
+
import path, { dirname, extname, relative, resolve } from "path";
|
|
4
|
+
import vue from "@vitejs/plugin-vue";
|
|
5
|
+
import { createFilter, defineConfig as defineConfig$1, loadConfigFromFile, normalizePath } from "vite";
|
|
6
|
+
import { mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs";
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { toHtml } from "hast-util-to-html";
|
|
9
|
+
import { glob } from "tinyglobby";
|
|
10
|
+
import pm from "picomatch";
|
|
11
|
+
import { unified } from "unified";
|
|
12
|
+
import { select } from "hast-util-select";
|
|
13
|
+
import { toText } from "hast-util-to-text";
|
|
14
|
+
import remarkParse from "remark-parse";
|
|
15
|
+
import remarkRehype from "remark-rehype";
|
|
16
|
+
import rehypeStringify from "rehype-stringify";
|
|
17
|
+
import matter from "gray-matter";
|
|
18
|
+
//#region src/utils.ts
|
|
19
|
+
function headConfigToHast(head) {
|
|
20
|
+
return head.map(([tagName, a, c]) => {
|
|
21
|
+
return {
|
|
22
|
+
type: "element",
|
|
23
|
+
tagName,
|
|
24
|
+
properties: a,
|
|
25
|
+
children: c == null ? [] : [{
|
|
26
|
+
type: "text",
|
|
27
|
+
value: c
|
|
28
|
+
}]
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function headConfigStringify(head) {
|
|
33
|
+
return toHtml(headConfigToHast(head));
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/plugins/build.ts
|
|
37
|
+
const EXCLUDE_DIRS = new Set([
|
|
38
|
+
".fumi",
|
|
39
|
+
".git",
|
|
40
|
+
"dist",
|
|
41
|
+
"node_modules",
|
|
42
|
+
"packages",
|
|
43
|
+
"public"
|
|
44
|
+
]);
|
|
45
|
+
function discoverPages(root) {
|
|
46
|
+
const pages = [];
|
|
47
|
+
function walk(dir) {
|
|
48
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) if (entry.isDirectory()) {
|
|
49
|
+
if (!EXCLUDE_DIRS.has(entry.name)) walk(resolve(dir, entry.name));
|
|
50
|
+
} else if (entry.isFile()) {
|
|
51
|
+
const ext = extname(entry.name);
|
|
52
|
+
const isUpperCase = entry.name[0] === entry.name[0].toUpperCase() && entry.name[0] !== entry.name[0].toLowerCase();
|
|
53
|
+
if ((ext === ".md" || ext === ".vue") && !isUpperCase) {
|
|
54
|
+
const filePath = resolve(dir, entry.name);
|
|
55
|
+
let url = "/" + relative(root, filePath).replace(/\\/g, "/").replace(/\.(md|vue)$/, "");
|
|
56
|
+
if (url.endsWith("/index")) url = url.slice(0, -6) || "/";
|
|
57
|
+
pages.push({
|
|
58
|
+
url,
|
|
59
|
+
filePath
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
walk(root);
|
|
65
|
+
return pages;
|
|
66
|
+
}
|
|
67
|
+
function urlToInputKey(url) {
|
|
68
|
+
return url === "/" ? "__app/index" : `__app${url}`;
|
|
69
|
+
}
|
|
70
|
+
function urlToOutPath(outDir, url) {
|
|
71
|
+
if (url === "/") return resolve(outDir, "index.html");
|
|
72
|
+
return resolve(outDir, url.slice(1) + ".html");
|
|
73
|
+
}
|
|
74
|
+
function build(config) {
|
|
75
|
+
let outDir;
|
|
76
|
+
return {
|
|
77
|
+
name: "fumi:build",
|
|
78
|
+
apply: "build",
|
|
79
|
+
configEnvironment(name, config) {
|
|
80
|
+
const cwd = process.cwd();
|
|
81
|
+
const pages = discoverPages(cwd);
|
|
82
|
+
if (name === "client") return { build: { rolldownOptions: {
|
|
83
|
+
preserveEntrySignatures: "strict",
|
|
84
|
+
input: {
|
|
85
|
+
index: resolve(cwd, ".fumi/index.html"),
|
|
86
|
+
...Object.fromEntries(pages.map(({ url }) => [urlToInputKey(url), `/${urlToInputKey(url)}.js`]))
|
|
87
|
+
},
|
|
88
|
+
output: {
|
|
89
|
+
entryFileNames: (chunk) => {
|
|
90
|
+
if (chunk.name.startsWith("__app/")) return chunk.name + ".js";
|
|
91
|
+
return "assets/[name]-[hash].js";
|
|
92
|
+
},
|
|
93
|
+
chunkFileNames: "assets/[name]-[hash].js",
|
|
94
|
+
assetFileNames: "assets/[name]-[hash].[ext]"
|
|
95
|
+
}
|
|
96
|
+
} } };
|
|
97
|
+
if (name === "ssr") return { build: {
|
|
98
|
+
outDir: `${config.build?.outDir ?? "dist"}/.fumi`,
|
|
99
|
+
emptyOutDir: false,
|
|
100
|
+
rolldownOptions: {
|
|
101
|
+
input: {
|
|
102
|
+
ssr: "@tenjot/fumi/ssr",
|
|
103
|
+
...Object.fromEntries(pages.map(({ url }) => [urlToInputKey(url), `/${urlToInputKey(url)}.js`]))
|
|
104
|
+
},
|
|
105
|
+
output: {
|
|
106
|
+
entryFileNames: (chunk) => {
|
|
107
|
+
if (chunk.name.startsWith("__app/")) return chunk.name + ".js";
|
|
108
|
+
return "[name].js";
|
|
109
|
+
},
|
|
110
|
+
chunkFileNames: "assets/[name]-[hash].js",
|
|
111
|
+
assetFileNames: "assets/[name]-[hash][ext]"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} };
|
|
115
|
+
},
|
|
116
|
+
configResolved(config) {
|
|
117
|
+
outDir = config.build.outDir;
|
|
118
|
+
},
|
|
119
|
+
async closeBundle() {
|
|
120
|
+
if (this.environment?.name !== "ssr") return;
|
|
121
|
+
const cwd = process.cwd();
|
|
122
|
+
const pages = discoverPages(cwd);
|
|
123
|
+
const ssrOutDir = outDir;
|
|
124
|
+
const clientOutDir = resolve(ssrOutDir, "..");
|
|
125
|
+
const template = await readFile(resolve(ssrOutDir, "index.html"), "utf-8");
|
|
126
|
+
const { render } = await import(
|
|
127
|
+
/* @vite-ignore */
|
|
128
|
+
resolve(ssrOutDir, "ssr.js") + `?t=${Date.now()}`
|
|
129
|
+
);
|
|
130
|
+
console.log("\nRendering pages...");
|
|
131
|
+
for (const { url } of pages) {
|
|
132
|
+
const { default: component, __pageData: data } = await import(
|
|
133
|
+
/* @vite-ignore */
|
|
134
|
+
resolve(ssrOutDir, `.${pathToPageComponentPath(url)}`)
|
|
135
|
+
);
|
|
136
|
+
const appHtml = await render(url, component, data);
|
|
137
|
+
if (appHtml === null) {
|
|
138
|
+
console.log(` skip ${url}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const appHead = headConfigStringify(configToHeadConfig(data));
|
|
142
|
+
const html = template.replace("<!--app-html-->", appHtml).replace("<!--app-head-->", appHead);
|
|
143
|
+
const outPath = urlToOutPath(clientOutDir, url);
|
|
144
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
145
|
+
writeFileSync(outPath, html);
|
|
146
|
+
console.log(` ${url} → ${relative(cwd, outPath)}`);
|
|
147
|
+
}
|
|
148
|
+
rmSync(ssrOutDir, { recursive: true });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/plugins/markdown.ts
|
|
154
|
+
const extractTitle = function() {
|
|
155
|
+
return (tree, file) => {
|
|
156
|
+
const node = select("h1", tree);
|
|
157
|
+
if (node) file.data.title = toText(node);
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
function createProcessor(options = {}) {
|
|
161
|
+
let remark = unified().use(remarkParse);
|
|
162
|
+
if (options.remarkPlugins?.length) remark = remark.use(options.remarkPlugins);
|
|
163
|
+
let rehype = remark.use(remarkRehype, { allowDangerousHtml: true });
|
|
164
|
+
if (options.rehypePlugins?.length) rehype = rehype.use(options.rehypePlugins);
|
|
165
|
+
rehype.use(extractTitle);
|
|
166
|
+
return rehype.use(rehypeStringify, { allowDangerousHtml: true });
|
|
167
|
+
}
|
|
168
|
+
function markdown(options, config) {
|
|
169
|
+
const processor = createProcessor(options);
|
|
170
|
+
let root;
|
|
171
|
+
return {
|
|
172
|
+
name: "fumi:markdown",
|
|
173
|
+
configResolved(config) {
|
|
174
|
+
root = config.root;
|
|
175
|
+
},
|
|
176
|
+
transform: {
|
|
177
|
+
filter: { id: /\.md$/ },
|
|
178
|
+
async handler(code, id) {
|
|
179
|
+
const { content, data: frontmatter } = matter(code);
|
|
180
|
+
const file = await processor.process(content);
|
|
181
|
+
const html = JSON.stringify(`<div class="fumi-markdown-content">${file.toString()}</div>`);
|
|
182
|
+
const pageConfig = { ...frontmatter };
|
|
183
|
+
if (pageConfig.title == null && file.data.title != null) pageConfig.title = file.data.title;
|
|
184
|
+
const resolvedConfig = resolveFumiConfig(config, pageConfig);
|
|
185
|
+
const data = {
|
|
186
|
+
path: id.replace("index.md", "index.html").replace(".md", ".html").replace(root, ""),
|
|
187
|
+
isNotFound: false,
|
|
188
|
+
frontmatter,
|
|
189
|
+
...resolvedConfig
|
|
190
|
+
};
|
|
191
|
+
return `import { createStaticVNode } from "vue";
|
|
192
|
+
export default { render: () => createStaticVNode(${html}, 1) };
|
|
193
|
+
export const __pageData = JSON.parse(${JSON.stringify(JSON.stringify(data))})`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/plugins/data-loader.ts
|
|
200
|
+
function dataLoader(markdownOptions, config) {
|
|
201
|
+
const depToLoaderModuleIdsMap = /* @__PURE__ */ new Map();
|
|
202
|
+
const idToLoaderModulesMap = /* @__PURE__ */ new Map();
|
|
203
|
+
return {
|
|
204
|
+
name: "fumi:data-loader",
|
|
205
|
+
sharedDuringBuild: true,
|
|
206
|
+
load: {
|
|
207
|
+
filter: { id: /\.data\.[tj]s$/ },
|
|
208
|
+
async handler(id) {
|
|
209
|
+
const root = this.environment.config.root;
|
|
210
|
+
const isBuild = this.environment.config.command === "build";
|
|
211
|
+
let loader;
|
|
212
|
+
const existing = idToLoaderModulesMap.get(id);
|
|
213
|
+
if (existing) loader = existing;
|
|
214
|
+
else {
|
|
215
|
+
const res = await loadConfigFromFile({}, id);
|
|
216
|
+
if (!res?.config) return null;
|
|
217
|
+
if (!isBuild) for (const dep of res.dependencies) {
|
|
218
|
+
const depPath = normalizePath(path.resolve(dep));
|
|
219
|
+
let set = depToLoaderModuleIdsMap.get(depPath);
|
|
220
|
+
if (!set) depToLoaderModuleIdsMap.set(depPath, set = /* @__PURE__ */ new Set());
|
|
221
|
+
set.add(id);
|
|
222
|
+
}
|
|
223
|
+
loader = res.config;
|
|
224
|
+
if (!isBuild) idToLoaderModulesMap.set(id, loader);
|
|
225
|
+
}
|
|
226
|
+
let data;
|
|
227
|
+
if (loader.watch) {
|
|
228
|
+
const watchFiles = (await glob(loader.watch, {
|
|
229
|
+
absolute: true,
|
|
230
|
+
expandDirectories: true,
|
|
231
|
+
ignore: [
|
|
232
|
+
"**/node_modules/**",
|
|
233
|
+
"**/dist/**",
|
|
234
|
+
"**/.fumi/**"
|
|
235
|
+
]
|
|
236
|
+
})).sort();
|
|
237
|
+
const processor = createProcessor(markdownOptions);
|
|
238
|
+
data = await loader.load(watchFiles, async (path) => {
|
|
239
|
+
const { content, data: frontmatter } = matter(await readFile(path, "utf-8"));
|
|
240
|
+
const file = await processor.process(content);
|
|
241
|
+
const pageConfig = { ...frontmatter };
|
|
242
|
+
if (pageConfig.title == null && file.data.title != null) pageConfig.title = file.data.title;
|
|
243
|
+
const resolvedConfig = resolveFumiConfig(config, pageConfig);
|
|
244
|
+
return {
|
|
245
|
+
path: path.replace("index.md", "index.html").replace(".md", ".html").replace(root, ""),
|
|
246
|
+
isNotFound: false,
|
|
247
|
+
...resolvedConfig
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
} else data = await loader.load();
|
|
251
|
+
if (!isBuild) idToLoaderModulesMap.set(id, loader);
|
|
252
|
+
return `export const data = JSON.parse(${JSON.stringify(JSON.stringify(data))})`;
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
hotUpdate({ file, modules: existingMods }) {
|
|
256
|
+
if (this.environment.name !== "client") return;
|
|
257
|
+
const modules = [];
|
|
258
|
+
const normalizedFile = normalizePath(file);
|
|
259
|
+
if (depToLoaderModuleIdsMap.has(normalizedFile)) for (const id of depToLoaderModuleIdsMap.get(normalizedFile) ?? []) {
|
|
260
|
+
idToLoaderModulesMap.delete(id);
|
|
261
|
+
const mod = this.environment.moduleGraph.getModuleById(id);
|
|
262
|
+
if (mod) modules.push(mod);
|
|
263
|
+
}
|
|
264
|
+
for (const [id, loader] of idToLoaderModulesMap.entries()) if (loader.watch?.length && pm(loader.watch)(normalizedFile)) {
|
|
265
|
+
const mod = this.environment.moduleGraph.getModuleById(id);
|
|
266
|
+
if (mod) modules.push(mod);
|
|
267
|
+
}
|
|
268
|
+
return modules.length ? [...existingMods, ...modules] : void 0;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
//#endregion
|
|
273
|
+
//#region src/plugins/dev-server.ts
|
|
274
|
+
const isCss = createFilter([/\.css(?:$|\?)/], [/[?&](?:worker|sharedworker|raw|url)\b/, /[?&]commonjs-proxy/]);
|
|
275
|
+
function devServer(config) {
|
|
276
|
+
return {
|
|
277
|
+
name: "fumi:dev-server",
|
|
278
|
+
apply: "serve",
|
|
279
|
+
configureServer(server) {
|
|
280
|
+
return dev(server, config);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function dev(server, config) {
|
|
285
|
+
const root = server.config.root;
|
|
286
|
+
const fumiRoot = resolve(root, ".fumi");
|
|
287
|
+
const ssrEnv = server.environments.ssr;
|
|
288
|
+
const clientEnv = server.environments.client;
|
|
289
|
+
return () => {
|
|
290
|
+
server.middlewares.use(async (req, res, next) => {
|
|
291
|
+
try {
|
|
292
|
+
const url = req.originalUrl ?? req.url ?? "/";
|
|
293
|
+
if (url.endsWith(".map")) return next();
|
|
294
|
+
if (url === "/favicon.ico") return next();
|
|
295
|
+
if (url.startsWith("/__app/")) return next();
|
|
296
|
+
let html = readFileSync(resolve(fumiRoot, "index.html"), "utf-8");
|
|
297
|
+
html = await server.transformIndexHtml(url, html);
|
|
298
|
+
const { render } = await ssrEnv.runner.import("@tenjot/fumi/ssr");
|
|
299
|
+
const pageComponentPath = pathToPageComponentPath(url);
|
|
300
|
+
const { default: component, __pageData } = await ssrEnv.runner.import(pageComponentPath).catch(async (e) => {
|
|
301
|
+
if (e.code !== "ERR_LOAD_URL") throw e;
|
|
302
|
+
const page = await ssrEnv.runner.import("/__app/404.js").catch(() => void 0);
|
|
303
|
+
return {
|
|
304
|
+
default: page?.default,
|
|
305
|
+
__pageData: {
|
|
306
|
+
...page?.__pageData,
|
|
307
|
+
path: url,
|
|
308
|
+
isNotFound: true
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
const appHtml = await render(url, component, __pageData);
|
|
313
|
+
if (appHtml === null) return next();
|
|
314
|
+
const pageMod = await clientEnv.moduleGraph.getModuleByUrl("/.fumi/App.vue");
|
|
315
|
+
const styleTag = pageMod ? renderCss(pageMod) : [];
|
|
316
|
+
const headTag = configToHeadConfig(__pageData);
|
|
317
|
+
const appHead = headConfigStringify([...styleTag, ...headTag]);
|
|
318
|
+
html = html.replace("<!--app-html-->", appHtml);
|
|
319
|
+
html = html.replace("<!--app-head-->", appHead);
|
|
320
|
+
res.writeHead(component != null ? 200 : 404, { "Content-Type": "text/html" });
|
|
321
|
+
res.end(html);
|
|
322
|
+
} catch (e) {
|
|
323
|
+
server.ssrFixStacktrace(e);
|
|
324
|
+
console.error(e);
|
|
325
|
+
next(e);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const viteCssRe = /\bconst __vite__css\s*=\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`[\s\S]*?`)/;
|
|
331
|
+
const viteIdRe = /\bconst __vite__id\s*=\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`[\s\S]*?`)/;
|
|
332
|
+
function renderCss(entry, visited = /* @__PURE__ */ new Set()) {
|
|
333
|
+
const result = [];
|
|
334
|
+
for (const mod of entry.importedModules) {
|
|
335
|
+
if (mod.id === null || visited.has(mod.id)) continue;
|
|
336
|
+
visited.add(mod.id);
|
|
337
|
+
if (isCss(mod.url)) {
|
|
338
|
+
const style = renderCssMod(mod);
|
|
339
|
+
if (!style) continue;
|
|
340
|
+
result.push(style);
|
|
341
|
+
} else result.push(...renderCss(mod, visited));
|
|
342
|
+
}
|
|
343
|
+
return result.filter(Boolean);
|
|
344
|
+
}
|
|
345
|
+
function renderCssMod(mod) {
|
|
346
|
+
const code = mod.transformResult?.code;
|
|
347
|
+
if (!code) return null;
|
|
348
|
+
const css = viteCssRe.exec(code);
|
|
349
|
+
if (!css) return null;
|
|
350
|
+
const id = viteIdRe.exec(code);
|
|
351
|
+
if (!id) return null;
|
|
352
|
+
return [
|
|
353
|
+
"style",
|
|
354
|
+
{ "data-vite-dev-id": JSON.parse(id[1]) },
|
|
355
|
+
JSON.parse(css[1])
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/plugins/page.ts
|
|
360
|
+
function page(config) {
|
|
361
|
+
let root;
|
|
362
|
+
return [{
|
|
363
|
+
name: "fumi:page",
|
|
364
|
+
configResolved(config) {
|
|
365
|
+
root = config.root;
|
|
366
|
+
},
|
|
367
|
+
resolveId: {
|
|
368
|
+
filter: { id: /^\/__app\/.*\.js(?:\.(?:md|vue))?$/ },
|
|
369
|
+
handler(source) {
|
|
370
|
+
const path = source.replace(/\.(?:md|vue)$/, "").replace(/^\/__app\//, "").replace(/\.js$/, "");
|
|
371
|
+
const vuePath = resolve(root, `${path}.vue`);
|
|
372
|
+
if (existsFile(vuePath)) return vuePath;
|
|
373
|
+
const mdPath = resolve(root, `${path}.md`);
|
|
374
|
+
if (existsFile(mdPath)) return mdPath;
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}];
|
|
379
|
+
}
|
|
380
|
+
function existsFile(path) {
|
|
381
|
+
try {
|
|
382
|
+
return statSync(path).isFile();
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/config.ts
|
|
389
|
+
/**
|
|
390
|
+
* Viteの設定。
|
|
391
|
+
* Fumiのために幾つかの設定が追加・上書きされる。
|
|
392
|
+
*/
|
|
393
|
+
function defineConfig(userConfig = {}) {
|
|
394
|
+
const { vite: viteUserConfig = {}, vue: vueOptions, markdown: markdownOptions = {}, ...fumiConfig } = userConfig;
|
|
395
|
+
const cwd = process.cwd();
|
|
396
|
+
return defineConfig$1({
|
|
397
|
+
...viteUserConfig,
|
|
398
|
+
plugins: [
|
|
399
|
+
vue(vueOptions),
|
|
400
|
+
devServer(fumiConfig),
|
|
401
|
+
build(fumiConfig),
|
|
402
|
+
page(fumiConfig),
|
|
403
|
+
markdown(markdownOptions, fumiConfig),
|
|
404
|
+
dataLoader(markdownOptions, fumiConfig),
|
|
405
|
+
...viteUserConfig.plugins ?? []
|
|
406
|
+
],
|
|
407
|
+
resolve: {
|
|
408
|
+
...viteUserConfig.resolve,
|
|
409
|
+
alias: {
|
|
410
|
+
...viteUserConfig.resolve?.alias,
|
|
411
|
+
"@app": resolve(cwd, ".fumi/App.vue")
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function resolveFumiConfig(global, page) {
|
|
417
|
+
const title = page.title ?? global.title;
|
|
418
|
+
let titleTemplate = page.titleTemplate === false ? void 0 : page.titleTemplate ?? (global.titleTemplate === false ? void 0 : global.titleTemplate);
|
|
419
|
+
if (title == null) {
|
|
420
|
+
if (titleTemplate?.includes(":title")) titleTemplate = void 0;
|
|
421
|
+
} else if (!titleTemplate?.includes(":title")) titleTemplate = void 0;
|
|
422
|
+
const head = [...global.head ?? [], ...page.head ?? []];
|
|
423
|
+
return {
|
|
424
|
+
title,
|
|
425
|
+
titleTemplate,
|
|
426
|
+
description: page.description ?? global.description,
|
|
427
|
+
head: head.length ? head : void 0,
|
|
428
|
+
lang: page.lang ?? global.lang
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/content-loader.ts
|
|
433
|
+
function defineLoader(dataLoader) {
|
|
434
|
+
return dataLoader;
|
|
435
|
+
}
|
|
436
|
+
//#endregion
|
|
437
|
+
export { defineConfig, defineLoader };
|
package/dist/main.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/main.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { r as loadPage } from "./router-DZgDu6-U.mjs";
|
|
2
|
+
import { n as resolveTitle } from "./config-BqdJHMUf.mjs";
|
|
3
|
+
import { t as createApp } from "./app-DvBaD5aw.mjs";
|
|
4
|
+
import { markRaw, watchEffect } from "vue";
|
|
5
|
+
//#region src/client/head.ts
|
|
6
|
+
function useHead(route) {
|
|
7
|
+
let isFirstUpdate = true;
|
|
8
|
+
let elements = [];
|
|
9
|
+
function updateHead(tags) {
|
|
10
|
+
if (isFirstUpdate) {
|
|
11
|
+
isFirstUpdate = false;
|
|
12
|
+
const children = [...document.head.children];
|
|
13
|
+
for (const tag of tags) {
|
|
14
|
+
const newEl = createHeadElement(tag);
|
|
15
|
+
const el = children.find((el) => el.isEqualNode(newEl));
|
|
16
|
+
if (el != null) elements.push(el);
|
|
17
|
+
}
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const newElements = tags.map(createHeadElement);
|
|
21
|
+
for (const [oldIndex, oldEl] of elements.entries()) {
|
|
22
|
+
const matched = newElements.findIndex((el) => el?.isEqualNode(oldEl ?? null));
|
|
23
|
+
if (matched !== -1) delete newElements[matched];
|
|
24
|
+
else {
|
|
25
|
+
oldEl?.remove();
|
|
26
|
+
delete elements[oldIndex];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const el of newElements) if (el) document.head.appendChild(el);
|
|
30
|
+
elements = [...elements, ...newElements].filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
watchEffect(() => {
|
|
33
|
+
const config = route.data;
|
|
34
|
+
const title = resolveTitle(config);
|
|
35
|
+
if (title !== document.title && title) document.title = title;
|
|
36
|
+
const metaDescElm = document.querySelector("meta[name=description]");
|
|
37
|
+
if (metaDescElm) {
|
|
38
|
+
if (!config.description) metaDescElm.remove();
|
|
39
|
+
else if (metaDescElm.getAttribute("content") !== config.description) metaDescElm.setAttribute("content", config.description);
|
|
40
|
+
} else if (config.description) createHeadElement(["meta", {
|
|
41
|
+
name: "description",
|
|
42
|
+
content: config.description
|
|
43
|
+
}]);
|
|
44
|
+
updateHead(config.head ?? []);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function createHeadElement([tag, attrs, innerHTML]) {
|
|
48
|
+
const el = document.createElement(tag);
|
|
49
|
+
for (const key in attrs) el.setAttribute(key, attrs[key]);
|
|
50
|
+
if (innerHTML) el.innerHTML = innerHTML;
|
|
51
|
+
if (tag === "script" && attrs.async == null) el.async = false;
|
|
52
|
+
return el;
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/client/main.ts
|
|
56
|
+
const path = location.pathname;
|
|
57
|
+
loadPage(path).then(({ default: cmp, __pageData }) => {
|
|
58
|
+
const { app, router } = createApp({
|
|
59
|
+
path,
|
|
60
|
+
component: cmp ? markRaw(cmp) : null,
|
|
61
|
+
data: __pageData
|
|
62
|
+
});
|
|
63
|
+
useHead(router.route);
|
|
64
|
+
app.mount("#app");
|
|
65
|
+
});
|
|
66
|
+
//#endregion
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Options } from "@vitejs/plugin-vue";
|
|
2
|
+
import { UserConfig } from "vite";
|
|
3
|
+
import { Component, Ref } from "vue";
|
|
4
|
+
import { PluggableList } from "unified";
|
|
5
|
+
|
|
6
|
+
//#region src/plugins/markdown.d.ts
|
|
7
|
+
interface MarkdownOptions {
|
|
8
|
+
/** remark-parse の後に適用する remark プラグイン */
|
|
9
|
+
remarkPlugins?: PluggableList;
|
|
10
|
+
/** remark-rehype の後に適用する rehype プラグイン */
|
|
11
|
+
rehypePlugins?: PluggableList;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/config.d.ts
|
|
15
|
+
type HeadConfig = [tag: string, attr: Record<string, string>] | [tag: string, attr: Record<string, string>, content: string];
|
|
16
|
+
interface FumiConfig {
|
|
17
|
+
/** サイトタイトル。Frontmatterで上書き可能。 */
|
|
18
|
+
title?: string;
|
|
19
|
+
/**
|
|
20
|
+
* サイトタイトルテンプレート。
|
|
21
|
+
* `:title`シンボルがFrontmatterまたは`h1`から推測されたタイトルが挿入される。
|
|
22
|
+
* `false`を設定するとテンプレートを無効にします。
|
|
23
|
+
*/
|
|
24
|
+
titleTemplate?: string | false;
|
|
25
|
+
/** サイトの説明。Frontmatterで上書き可能 */
|
|
26
|
+
description?: string;
|
|
27
|
+
/** `<head>`に追加する要素。 */
|
|
28
|
+
head?: HeadConfig[];
|
|
29
|
+
/** サイトの言語属性 */
|
|
30
|
+
lang?: string;
|
|
31
|
+
}
|
|
32
|
+
interface ResolvedFumiConfig extends FumiConfig {
|
|
33
|
+
titleTemplate?: string;
|
|
34
|
+
}
|
|
35
|
+
interface FumiUserConfig extends FumiConfig {
|
|
36
|
+
vite?: UserConfig;
|
|
37
|
+
vue?: Options;
|
|
38
|
+
markdown?: MarkdownOptions;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Viteの設定。
|
|
42
|
+
* Fumiのために幾つかの設定が追加・上書きされる。
|
|
43
|
+
*/
|
|
44
|
+
declare function defineConfig$1(userConfig?: FumiUserConfig): UserConfig;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/client/data.d.ts
|
|
47
|
+
/** Page-level metadata */
|
|
48
|
+
interface PageData extends ResolvedFumiConfig {
|
|
49
|
+
/** 現在のページのパス (`/`は`/index.html`となる) */
|
|
50
|
+
path: string;
|
|
51
|
+
/** もしページが見つからない場合trueになる */
|
|
52
|
+
isNotFound: boolean;
|
|
53
|
+
/** Markdown frontmatter */
|
|
54
|
+
frontmatter?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
/** Fumi metadata */
|
|
57
|
+
interface SiteData {
|
|
58
|
+
/** Page-level metadata */
|
|
59
|
+
page: Ref<PageData>;
|
|
60
|
+
}
|
|
61
|
+
/** サイトのメタデータ */
|
|
62
|
+
declare function useData(): SiteData;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/client/router.d.ts
|
|
65
|
+
interface Route {
|
|
66
|
+
/** 現在のページのパス (`new URL(location.href).pathname`と同じ) */
|
|
67
|
+
path: string;
|
|
68
|
+
/** 現在のページのコンポーネント */
|
|
69
|
+
component: Component | null;
|
|
70
|
+
/** 現在のページのメタデータ */
|
|
71
|
+
data: PageData;
|
|
72
|
+
}
|
|
73
|
+
interface Router {
|
|
74
|
+
/** 現在のルート情報 */
|
|
75
|
+
route: Route;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* ルーター
|
|
79
|
+
* ナビゲーションにはNavigation APIを使用します。
|
|
80
|
+
*/
|
|
81
|
+
declare function useRouter(): Router;
|
|
82
|
+
/** 現在のルート情報 */
|
|
83
|
+
declare function useRoute(): Route;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { useData as a, defineConfig$1 as c, SiteData as i, useRouter as n, FumiUserConfig as o, PageData as r, HeadConfig as s, useRoute as t };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { inject, markRaw, reactive } from "vue";
|
|
2
|
+
//#region src/client/router.ts
|
|
3
|
+
const RouterSymbol = Symbol();
|
|
4
|
+
function createRouter(initialRoute) {
|
|
5
|
+
const route = reactive({
|
|
6
|
+
path: initialRoute?.path ?? "/",
|
|
7
|
+
component: initialRoute?.component ? markRaw(initialRoute.component) : null,
|
|
8
|
+
data: initialRoute?.data ?? {
|
|
9
|
+
path: "/",
|
|
10
|
+
isNotFound: true
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
if (typeof navigation !== "undefined") navigation.addEventListener("navigate", (e) => {
|
|
14
|
+
if (!e.canIntercept || e.hashChange || e.downloadRequest !== null) return;
|
|
15
|
+
const url = new URL(e.destination.url);
|
|
16
|
+
if (url.origin !== location.origin) return;
|
|
17
|
+
if (import.meta.env.DEV && url.pathname.startsWith("/__fumi/editor")) return;
|
|
18
|
+
e.intercept({ async handler() {
|
|
19
|
+
const page = await loadPage(url.pathname);
|
|
20
|
+
route.component = page.default ? markRaw(page.default) : null;
|
|
21
|
+
route.path = url.pathname;
|
|
22
|
+
route.data = page.__pageData;
|
|
23
|
+
} });
|
|
24
|
+
});
|
|
25
|
+
return { route };
|
|
26
|
+
}
|
|
27
|
+
function pathToPageComponentPath(pathname) {
|
|
28
|
+
let path = pathname.replace(/\.html$/, "");
|
|
29
|
+
if (path === "/" || path === "") path = "/index";
|
|
30
|
+
else if (path.endsWith("/")) path = path.slice(0, -1) + "/index";
|
|
31
|
+
return `/__app${path}.js`;
|
|
32
|
+
}
|
|
33
|
+
async function loadPage(pathname) {
|
|
34
|
+
try {
|
|
35
|
+
return await import(
|
|
36
|
+
/* @vite-ignore */
|
|
37
|
+
pathToPageComponentPath(pathname)
|
|
38
|
+
);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (!(e instanceof TypeError)) throw e;
|
|
41
|
+
const page = await import(
|
|
42
|
+
/* @vite-ignore */
|
|
43
|
+
"/__app/404.js"
|
|
44
|
+
);
|
|
45
|
+
return {
|
|
46
|
+
default: page?.default,
|
|
47
|
+
__pageData: {
|
|
48
|
+
...page?.__pageData,
|
|
49
|
+
url: pathname,
|
|
50
|
+
isNotFound: true
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* ルーター
|
|
57
|
+
* ナビゲーションにはNavigation APIを使用します。
|
|
58
|
+
*/
|
|
59
|
+
function useRouter() {
|
|
60
|
+
const router = inject(RouterSymbol);
|
|
61
|
+
if (!router) throw new Error("useRouter() is called without provider.");
|
|
62
|
+
return router;
|
|
63
|
+
}
|
|
64
|
+
/** 現在のルート情報 */
|
|
65
|
+
function useRoute() {
|
|
66
|
+
return useRouter().route;
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { useRoute as a, pathToPageComponentPath as i, createRouter as n, useRouter as o, loadPage as r, RouterSymbol as t };
|
package/dist/ssr.d.mts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { r as PageData } from "./router-BEb_gb9D.mjs";
|
|
2
|
+
import { Component } from "vue";
|
|
3
|
+
|
|
4
|
+
//#region src/ssr.d.ts
|
|
5
|
+
/** FumiをSSR用にレンダリングする */
|
|
6
|
+
declare function render(url: string, component: Component, data: PageData): Promise<string>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { render };
|
package/dist/ssr.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { t as createApp } from "./app-DvBaD5aw.mjs";
|
|
2
|
+
import { renderToString } from "vue/server-renderer";
|
|
3
|
+
//#region src/ssr.ts
|
|
4
|
+
/** FumiをSSR用にレンダリングする */
|
|
5
|
+
async function render(url, component, data) {
|
|
6
|
+
const { app } = createApp({
|
|
7
|
+
path: url,
|
|
8
|
+
component,
|
|
9
|
+
data
|
|
10
|
+
});
|
|
11
|
+
return await renderToString(app);
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { render };
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tenjot/fumi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A static site generator for Vue + remark",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "tenjo-t",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"imports": {
|
|
9
|
+
"#editor/*": "./dist/editor/*"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"default": "./dist/index.mjs",
|
|
14
|
+
"types": "./dist/index.d.mts"
|
|
15
|
+
},
|
|
16
|
+
"./client": {
|
|
17
|
+
"default": "./dist/client.mjs",
|
|
18
|
+
"types": "./dist/client.d.mts"
|
|
19
|
+
},
|
|
20
|
+
"./main": {
|
|
21
|
+
"default": "./dist/main.mjs",
|
|
22
|
+
"types": "./dist/main.d.mts"
|
|
23
|
+
},
|
|
24
|
+
"./ssr": {
|
|
25
|
+
"default": "./dist/ssr.mjs",
|
|
26
|
+
"types": "./dist/ssr.d.mts"
|
|
27
|
+
},
|
|
28
|
+
"./ssr.mjs": {
|
|
29
|
+
"default": "./dist/ssr.mjs"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@types/node": "^25.4.0",
|
|
40
|
+
"@vitejs/plugin-vue": "^6.0.5",
|
|
41
|
+
"gray-matter": "^4.0.3",
|
|
42
|
+
"hast-util-select": "^6.0.4",
|
|
43
|
+
"hast-util-to-html": "^9.0.5",
|
|
44
|
+
"hast-util-to-text": "^4.0.2",
|
|
45
|
+
"picomatch": "^4.0.4",
|
|
46
|
+
"rehype-stringify": "^10.0.1",
|
|
47
|
+
"remark-parse": "^11.0.0",
|
|
48
|
+
"remark-rehype": "^11.1.1",
|
|
49
|
+
"tinyglobby": "^0.2.15",
|
|
50
|
+
"unified": "^11.0.5",
|
|
51
|
+
"vite": "^8.0.14",
|
|
52
|
+
"vue": "^3.5.34"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@tailwindcss/vite": "^4.3.0",
|
|
56
|
+
"@types/hast": "^3.0.4",
|
|
57
|
+
"@types/picomatch": "^4.0.2",
|
|
58
|
+
"tailwindcss": "^4.3.0",
|
|
59
|
+
"tsdown": "^0.21.2",
|
|
60
|
+
"typescript": "^6.0.3",
|
|
61
|
+
"unplugin-vue": "^7.1.1",
|
|
62
|
+
"vue-tsc": "^3.2.5"
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"dev": "tsdown -w",
|
|
66
|
+
"build": "tsdown"
|
|
67
|
+
}
|
|
68
|
+
}
|