@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 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 };
@@ -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 };
@@ -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 };
@@ -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
+ }