fimo-vite 0.21.0-experimental.1

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 ADDED
@@ -0,0 +1,24 @@
1
+ # fimo-vite
2
+
3
+ The framework-neutral Fimo integration for projects that build with Vite.
4
+ Install it alongside `fimo` at the same version:
5
+
6
+ ```bash
7
+ npm install fimo fimo-vite
8
+ ```
9
+
10
+ ```ts
11
+ import { defineConfig } from 'vite';
12
+ import { fimo } from 'fimo-vite';
13
+
14
+ export default defineConfig({
15
+ plugins: [fimo({ seo: true })],
16
+ });
17
+ ```
18
+
19
+ Nothing here depends on React, React Router, Svelte, or any other UI framework:
20
+ it is the config virtual module, the label/content dev bridge, the JSX
21
+ `data-fimo-id` tagger, the dev error overlay, and the opt-in `sitemap` /
22
+ `robots` / `seo` emitters. Framework packages compose it — React Router
23
+ projects use `fimo-react-router/vite`, which adds the router-specific
24
+ dependency-optimization behaviour on top of this plugin set.
@@ -0,0 +1,11 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Keeps generated schema + form clients in sync while the Vite dev server is
4
+ * running.
5
+ *
6
+ * Production builds are deliberately read-only. Explicit authoring commands
7
+ * (`fimo validate`, `fimo schemas push`, and `fimo forms push`) own initial
8
+ * generation before a build starts.
9
+ */
10
+ export default function contentSyncPlugin(): Plugin;
11
+ //# sourceMappingURL=content-sync.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { resolve } from 'path';
2
+ import { syncForms, syncSchemas } from 'fimo/sync';
3
+ /**
4
+ * Keeps generated schema + form clients in sync while the Vite dev server is
5
+ * running.
6
+ *
7
+ * Production builds are deliberately read-only. Explicit authoring commands
8
+ * (`fimo validate`, `fimo schemas push`, and `fimo forms push`) own initial
9
+ * generation before a build starts.
10
+ */
11
+ export default function contentSyncPlugin() {
12
+ let rootDir = process.cwd();
13
+ return {
14
+ name: 'vite-plugin-fimo-content-sync',
15
+ enforce: 'pre',
16
+ configResolved(config) {
17
+ rootDir = config.root;
18
+ },
19
+ configureServer(server) {
20
+ const schemasGlob = resolve(rootDir, 'src/schemas');
21
+ const formsGlob = resolve(rootDir, 'src/forms');
22
+ server.watcher.add([schemasGlob, formsGlob]);
23
+ server.watcher.on('add', onChange);
24
+ server.watcher.on('change', onChange);
25
+ server.watcher.on('unlink', onChange);
26
+ function onChange(file) {
27
+ if (!file.endsWith('.json')) {
28
+ return;
29
+ }
30
+ if (file.startsWith(schemasGlob)) {
31
+ syncSchemas({ cwd: rootDir, framework: 'vite', verbose: false });
32
+ return;
33
+ }
34
+ if (file.startsWith(formsGlob)) {
35
+ syncForms({ cwd: rootDir, verbose: false });
36
+ }
37
+ }
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ export default function dataIdPlugin(): Plugin;
3
+ //# sourceMappingURL=data-id.d.ts.map
@@ -0,0 +1,98 @@
1
+ import { createFilter } from '@rollup/pluginutils';
2
+ import { getLineColumn } from 'fimo/vite/data-id';
3
+ import { parseSync } from 'oxc-parser';
4
+ export default function dataIdPlugin() {
5
+ let config;
6
+ return {
7
+ name: 'vite-plugin-data-id',
8
+ enforce: 'pre',
9
+ configResolved(resolvedConfig) {
10
+ config = resolvedConfig;
11
+ },
12
+ async transform(code, id) {
13
+ if (config?.command === 'build') {
14
+ return null;
15
+ }
16
+ const filter = createFilter(/\.(jsx|tsx)$/);
17
+ if (!filter(id)) {
18
+ return null;
19
+ }
20
+ if (id.includes('components/ui/') || id.includes('.fimo/ui/')) {
21
+ return null;
22
+ }
23
+ try {
24
+ const result = parseSync(id, code);
25
+ const ast = result.program;
26
+ const insertions = [];
27
+ function visit(node) {
28
+ if (!node || typeof node !== 'object') {
29
+ return;
30
+ }
31
+ if (node.type === 'JSXOpeningElement') {
32
+ const attributes = node.attributes || [];
33
+ const hasId = attributes.some((attr) => attr.type === 'JSXAttribute' &&
34
+ attr.name?.type === 'JSXIdentifier' &&
35
+ attr.name?.name === 'data-fimo-id');
36
+ if (!hasId && node.start != null) {
37
+ const line = getLineColumn(code, node.start);
38
+ const relativePath = id.replace(process.cwd() + '/', '');
39
+ const fimoId = `${relativePath}#${line.line}-${line.column}`;
40
+ const nameEnd = getNameEnd(node);
41
+ insertions.push({
42
+ offset: nameEnd,
43
+ value: ` data-fimo-id="${fimoId}"`,
44
+ });
45
+ }
46
+ }
47
+ for (const key of Object.keys(node)) {
48
+ if (key === 'parent') {
49
+ continue;
50
+ }
51
+ const child = node[key];
52
+ if (Array.isArray(child)) {
53
+ for (const item of child) {
54
+ visit(item);
55
+ }
56
+ }
57
+ else if (typeof child === 'object' && child !== null) {
58
+ visit(child);
59
+ }
60
+ }
61
+ }
62
+ visit(ast);
63
+ if (insertions.length === 0) {
64
+ return null;
65
+ }
66
+ insertions.sort((a, b) => b.offset - a.offset);
67
+ let transformed = code;
68
+ for (const insertion of insertions) {
69
+ transformed = transformed.slice(0, insertion.offset) + insertion.value + transformed.slice(insertion.offset);
70
+ }
71
+ return {
72
+ code: transformed,
73
+ map: null,
74
+ };
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ },
80
+ };
81
+ }
82
+ /**
83
+ * Offset just past the element's name (or its type arguments), which is where
84
+ * an extra attribute can be spliced in without shifting any other token.
85
+ *
86
+ * The `<path>#<line>-<column>` id itself comes from `fimo/vite/data-id`, shared
87
+ * with the SvelteKit plugin, so the convention cannot drift between them.
88
+ */
89
+ function getNameEnd(openingElement) {
90
+ const name = openingElement.name;
91
+ if (name?.end != null) {
92
+ if (openingElement.typeParameters?.end != null) {
93
+ return openingElement.typeParameters.end;
94
+ }
95
+ return name.end;
96
+ }
97
+ return openingElement.start;
98
+ }
@@ -0,0 +1,12 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Vite plugin that exposes the user's `.fimo/config.json`:
4
+ * - As a virtual module `virtual:fimo-config` that browser-running package
5
+ * code (fimo/seo, fimo/app) imports to read `seo`, `routes`, etc. without
6
+ * relying on `readFileSync` at runtime.
7
+ *
8
+ * Edits to the project config invalidate the virtual module so HMR picks up
9
+ * the new values without a full reload.
10
+ */
11
+ export default function fimoConfigPlugin(): Plugin;
12
+ //# sourceMappingURL=fimo-config.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { readFileSync } from 'fs';
2
+ import { resolveFimoConfigPath } from 'fimo/config';
3
+ const VIRTUAL_ID = 'virtual:fimo-config';
4
+ const RESOLVED_ID = '\0' + VIRTUAL_ID;
5
+ /**
6
+ * Vite plugin that exposes the user's `.fimo/config.json`:
7
+ * - As a virtual module `virtual:fimo-config` that browser-running package
8
+ * code (fimo/seo, fimo/app) imports to read `seo`, `routes`, etc. without
9
+ * relying on `readFileSync` at runtime.
10
+ *
11
+ * Edits to the project config invalidate the virtual module so HMR picks up
12
+ * the new values without a full reload.
13
+ */
14
+ export default function fimoConfigPlugin() {
15
+ let configPath = resolveFimoConfigPath(process.cwd());
16
+ return {
17
+ name: 'vite-plugin-fimo-config',
18
+ config(userConfig) {
19
+ const root = typeof userConfig.root === 'string' ? userConfig.root : process.cwd();
20
+ configPath = resolveFimoConfigPath(root);
21
+ },
22
+ resolveId(id) {
23
+ if (id === VIRTUAL_ID) {
24
+ return RESOLVED_ID;
25
+ }
26
+ return null;
27
+ },
28
+ load(id) {
29
+ if (id !== RESOLVED_ID) {
30
+ return null;
31
+ }
32
+ this.addWatchFile(configPath);
33
+ try {
34
+ const raw = readFileSync(configPath, 'utf-8');
35
+ return `export default ${raw};`;
36
+ }
37
+ catch {
38
+ return 'export default {};';
39
+ }
40
+ },
41
+ transformIndexHtml(html) {
42
+ const lang = readHtmlLang(configPath);
43
+ if (/<html\b[^>]*\blang=/.test(html)) {
44
+ return html.replace(/(<html\b[^>]*\blang=["'])[^"']*(["'])/i, `$1${lang}$2`);
45
+ }
46
+ return html.replace(/<html\b([^>]*)>/i, `<html$1 lang="${lang}">`);
47
+ },
48
+ handleHotUpdate(ctx) {
49
+ if (ctx.file !== configPath) {
50
+ return;
51
+ }
52
+ const mod = ctx.server.moduleGraph.getModuleById(RESOLVED_ID);
53
+ if (mod) {
54
+ ctx.server.moduleGraph.invalidateModule(mod);
55
+ return [mod];
56
+ }
57
+ },
58
+ };
59
+ }
60
+ function readHtmlLang(configPath) {
61
+ try {
62
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
63
+ return (raw.i18n?.defaultLocale ?? 'en').replace('_', '-');
64
+ }
65
+ catch {
66
+ return 'en';
67
+ }
68
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ export default function overlayPlugin(): Plugin;
3
+ //# sourceMappingURL=overlay.d.ts.map
@@ -0,0 +1,153 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ // Compiled overlay runtime lives at `dist/runtime/*.js` once this package is
5
+ // built. The plugin lives at `dist/plugins/overlay.js`, so it walks back to
6
+ // `dist` and then into the sibling runtime directory.
7
+ function resolveRuntimeDir() {
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ return resolve(here, '..', 'runtime');
10
+ }
11
+ // Two virtual modules so a framework's script connector can wire both halves
12
+ // of the overlay:
13
+ //
14
+ // `virtual:fimo-overlay-runtime` — exports the runtime source as a string,
15
+ // to be embedded inline. Handles window.error + unhandledrejection + DOM
16
+ // overlay. Inline because we want it loaded as early as possible.
17
+ //
18
+ // `virtual:fimo-overlay-hmr` — a real ES module (side-effect import).
19
+ // Handles vite:error / vite:afterUpdate. Must be vite-transformed so
20
+ // `import.meta.hot` is defined; inline scripts bypass vite's transform.
21
+ //
22
+ // In production both resolve to no-ops so the overlay machinery is dev-only.
23
+ // The previous `transformIndexHtml` approach only fired when vite served an
24
+ // `index.html`, which react-router framework mode bypasses entirely.
25
+ const VIRTUAL_RUNTIME = 'virtual:fimo-overlay-runtime';
26
+ const VIRTUAL_HMR = 'virtual:fimo-overlay-hmr';
27
+ // When the user's code has a parse / load error at the time of a full page
28
+ // request (e.g. hard reload while the file is broken, or cold-loading the
29
+ // preview iframe against a broken sandbox), vite serves a 500 HTML response
30
+ // containing the structured error as a JS object literal:
31
+ //
32
+ // <script type="module">
33
+ // const error = { message, stack, id, frame, pluginCode, ... };
34
+ // const { ErrorOverlay } = await import("/@vite/client");
35
+ // document.body.appendChild(new ErrorOverlay(error));
36
+ // </script>
37
+ //
38
+ // In framework mode, none of the user's modules — including the inline
39
+ // overlay-runtime and the side-effect HMR import — ever load on this kind of
40
+ // response, so the parent UI is left blind. We extract that object literal
41
+ // and append a script that postMessages it to `window.parent` using the same
42
+ // `preview/error` + `app/error` shape the runtime uses, so the dashboard's
43
+ // overlay state machine fires identically to the HMR path.
44
+ function injectErrorForwarder(html) {
45
+ // Vite's error page structure:
46
+ // const error = { ...escaped JSON including the whole source as
47
+ // `pluginCode` (which contains its own `};` sequences) }
48
+ // <newline + spaces>try {
49
+ // Anchor on `try {` (which always immediately follows) so the lazy
50
+ // matcher backtracks past `}` sequences embedded inside the source.
51
+ // The trailing `;` is optional — vite relies on ASI here.
52
+ const match = html.match(/const error = (\{[\s\S]*?\})\s*;?\s*try\s*\{/);
53
+ if (!match)
54
+ return html;
55
+ const errorLiteral = match[1];
56
+ const script = '<script>(function(){try{var e=' +
57
+ errorLiteral +
58
+ ';if(window.parent&&window.parent!==window){' +
59
+ 'var p={heading:"Build Error",message:(e&&e.message)||"",stack:(e&&e.stack)||""};' +
60
+ 'window.parent.postMessage({type:"preview/error",payload:p},"*");' +
61
+ 'window.parent.postMessage({type:"app/error",payload:p},"*");' +
62
+ '}}catch(_){}}());</script>';
63
+ if (html.includes('</body>'))
64
+ return html.replace('</body>', script + '</body>');
65
+ return html + script;
66
+ }
67
+ export default function overlayPlugin() {
68
+ let isDev = false;
69
+ return {
70
+ name: 'vite-plugin-overlay',
71
+ config(_, env) {
72
+ isDev = env.command === 'serve';
73
+ },
74
+ resolveId(id) {
75
+ if (id === VIRTUAL_RUNTIME || id === VIRTUAL_HMR)
76
+ return id;
77
+ return undefined;
78
+ },
79
+ load(id) {
80
+ if (id === VIRTUAL_RUNTIME) {
81
+ if (!isDev)
82
+ return 'export default "";';
83
+ try {
84
+ const runtime = readFileSync(resolve(resolveRuntimeDir(), 'overlay-runtime.js'), 'utf-8');
85
+ return `export default ${JSON.stringify(runtime)};`;
86
+ }
87
+ catch {
88
+ return 'export default "";';
89
+ }
90
+ }
91
+ if (id === VIRTUAL_HMR) {
92
+ if (!isDev)
93
+ return 'export {};';
94
+ try {
95
+ return readFileSync(resolve(resolveRuntimeDir(), 'overlay-hmr.js'), 'utf-8');
96
+ }
97
+ catch {
98
+ return 'export {};';
99
+ }
100
+ }
101
+ return undefined;
102
+ },
103
+ configureServer(server) {
104
+ server.middlewares.use((_req, res, next) => {
105
+ const chunks = [];
106
+ const origWrite = res.write.bind(res);
107
+ const origEnd = res.end.bind(res);
108
+ let capturing = false;
109
+ // Capture every 500 response; sniff by body content in
110
+ // `injectErrorForwarder` since vite emits its error page without a
111
+ // Content-Type at the moment res.end() is called.
112
+ const shouldCapture = () => res.statusCode === 500;
113
+ // The Connect / Node response.write & end signatures accept Buffer
114
+ // | string | Uint8Array; we accept everything and normalise to Buffer
115
+ // so concat works regardless of how vite chose to emit the chunks.
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ res.write = function (chunk, ...rest) {
118
+ if (!capturing && shouldCapture())
119
+ capturing = true;
120
+ if (capturing) {
121
+ if (chunk)
122
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
123
+ return true;
124
+ }
125
+ return origWrite(chunk, ...rest);
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
+ };
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ res.end = function (chunk, ...rest) {
130
+ if (!capturing && shouldCapture())
131
+ capturing = true;
132
+ if (!capturing)
133
+ return origEnd(chunk, ...rest);
134
+ if (chunk)
135
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
136
+ const html = injectErrorForwarder(Buffer.concat(chunks).toString('utf8'));
137
+ const body = Buffer.from(html, 'utf8');
138
+ // Vite sets Content-Length before us; rewrite to match the new body.
139
+ try {
140
+ res.setHeader('content-length', String(body.byteLength));
141
+ }
142
+ catch {
143
+ // Headers already sent — ignore, content-length will be wrong but
144
+ // browsers tolerate trailing data on HTTP/1.1 keep-alive.
145
+ }
146
+ return origEnd(body, ...rest);
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ };
149
+ next();
150
+ });
151
+ },
152
+ };
153
+ }
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Emit a minimal `robots.txt` (allow-all + Sitemap reference if `seo.url` is
4
+ * set in `.fimo/config.json`). Serves at `/robots.txt` in dev; emits as a
5
+ * static asset at build time.
6
+ *
7
+ * Opt-in via `fimo({ robots: true })` in your `vite.config.ts`.
8
+ */
9
+ export default function robotsPlugin(): Plugin;
10
+ //# sourceMappingURL=robots.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { readFileSync } from 'fs';
2
+ import { resolveFimoConfigPath } from 'fimo/config';
3
+ function readSeoUrl() {
4
+ try {
5
+ const raw = readFileSync(resolveFimoConfigPath(process.cwd()), 'utf-8');
6
+ const config = JSON.parse(raw);
7
+ return config.seo?.url;
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ }
13
+ function buildRobotsTxt(baseUrl) {
14
+ const lines = ['User-agent: *', 'Allow: /'];
15
+ if (baseUrl) {
16
+ try {
17
+ const sitemapUrl = new URL('/sitemap.xml', baseUrl).toString();
18
+ lines.push('', `Sitemap: ${sitemapUrl}`);
19
+ }
20
+ catch {
21
+ // Invalid baseUrl — skip the Sitemap directive.
22
+ }
23
+ }
24
+ return lines.join('\n') + '\n';
25
+ }
26
+ /**
27
+ * Emit a minimal `robots.txt` (allow-all + Sitemap reference if `seo.url` is
28
+ * set in `.fimo/config.json`). Serves at `/robots.txt` in dev; emits as a
29
+ * static asset at build time.
30
+ *
31
+ * Opt-in via `fimo({ robots: true })` in your `vite.config.ts`.
32
+ */
33
+ export default function robotsPlugin() {
34
+ return {
35
+ name: 'fimo:robots',
36
+ configureServer(server) {
37
+ server.middlewares.use((req, res, next) => {
38
+ if (req.url?.split('?')[0] !== '/robots.txt') {
39
+ next();
40
+ return;
41
+ }
42
+ const baseUrl = readSeoUrl();
43
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
44
+ res.end(buildRobotsTxt(baseUrl));
45
+ });
46
+ },
47
+ generateBundle() {
48
+ const baseUrl = readSeoUrl();
49
+ this.emitFile({
50
+ type: 'asset',
51
+ fileName: 'robots.txt',
52
+ source: buildRobotsTxt(baseUrl),
53
+ });
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ export default function seoPlugin(): Plugin;
3
+ //# sourceMappingURL=seo.d.ts.map
@@ -0,0 +1,112 @@
1
+ import { getConfigServer } from 'fimo/config';
2
+ import { normalizePathname, resolveSeoForPath } from 'fimo/seo';
3
+ class HtmlTag {
4
+ tags = [];
5
+ addMeta(name, content) {
6
+ this.tags.push({ tag: 'meta', attrs: { name, content } });
7
+ }
8
+ addProperty(property, content) {
9
+ this.tags.push({ tag: 'meta', attrs: { property, content } });
10
+ }
11
+ addLink(rel, href) {
12
+ this.tags.push({ tag: 'link', attrs: { rel, href } });
13
+ }
14
+ list() {
15
+ return this.tags;
16
+ }
17
+ }
18
+ export default function seoPlugin() {
19
+ let rootDir;
20
+ return {
21
+ name: 'vite-plugin-seo',
22
+ configResolved(config) {
23
+ rootDir = config.root;
24
+ },
25
+ transformIndexHtml(html, ctx) {
26
+ try {
27
+ const config = getConfigServer(rootDir);
28
+ const pathname = normalizePathname(ctx?.path ?? '/');
29
+ const seo = resolveSeoForPath(pathname, config);
30
+ if (!seo) {
31
+ return html;
32
+ }
33
+ let nextHtml = html;
34
+ if (seo.title) {
35
+ if (/<title>[\s\S]*?<\/title>/.test(nextHtml)) {
36
+ nextHtml = nextHtml.replace(/<title>[\s\S]*?<\/title>/, `<title>${seo.title}</title>`);
37
+ }
38
+ else {
39
+ nextHtml = nextHtml.replace('<head>', `<head>\n <title>${seo.title}</title>`);
40
+ }
41
+ }
42
+ // Remove existing meta/link tags that we will re-inject to avoid duplication
43
+ // (e.g. when the source index.html already contains og:/twitter: tags from the user's original site)
44
+ nextHtml = nextHtml
45
+ .replace(/<meta\b[^>]*\bname=["']description["'][^>]*>/gi, '')
46
+ .replace(/<meta\b[^>]*\bname=["']robots["'][^>]*>/gi, '')
47
+ .replace(/<meta\b[^>]*\bname=["']theme-color["'][^>]*>/gi, '')
48
+ .replace(/<meta\b[^>]*\b(?:name|property)=["']og:[^"']*["'][^>]*>/gi, '')
49
+ .replace(/<meta\b[^>]*\b(?:name|property)=["']twitter:[^"']*["'][^>]*>/gi, '')
50
+ .replace(/<link\b[^>]*\brel=["']canonical["'][^>]*>/gi, '');
51
+ if (seo.favicon) {
52
+ nextHtml = nextHtml.replace(/<link\b[^>]*\brel=(?:"[^"]*icon[^"]*"|'[^']*icon[^']*'|[^>\s]*icon[^>\s]*)[^>]*>/gi, '');
53
+ }
54
+ const tags = new HtmlTag();
55
+ if (seo.description) {
56
+ tags.addMeta('description', seo.description);
57
+ }
58
+ if (seo.robots) {
59
+ tags.addMeta('robots', seo.robots);
60
+ }
61
+ if (seo.themeColor) {
62
+ tags.addMeta('theme-color', seo.themeColor);
63
+ }
64
+ if (seo.url) {
65
+ tags.addLink('canonical', seo.url);
66
+ }
67
+ if (seo.favicon) {
68
+ tags.addLink('icon', seo.favicon);
69
+ tags.addLink('apple-touch-icon', seo.favicon);
70
+ }
71
+ // Open Graph (must use property attribute, not name)
72
+ if (seo.title) {
73
+ tags.addProperty('og:title', seo.title);
74
+ }
75
+ if (seo.description) {
76
+ tags.addProperty('og:description', seo.description);
77
+ }
78
+ tags.addProperty('og:type', 'website');
79
+ if (seo.url) {
80
+ tags.addProperty('og:url', seo.url);
81
+ }
82
+ if (seo.siteName) {
83
+ tags.addProperty('og:site_name', seo.siteName);
84
+ }
85
+ if (seo.locale) {
86
+ tags.addProperty('og:locale', seo.locale);
87
+ }
88
+ if (seo.image) {
89
+ tags.addProperty('og:image', seo.image);
90
+ }
91
+ // Twitter
92
+ tags.addMeta('twitter:card', 'summary_large_image');
93
+ if (seo.twitterSite) {
94
+ tags.addMeta('twitter:site', seo.twitterSite);
95
+ }
96
+ if (seo.title) {
97
+ tags.addMeta('twitter:title', seo.title);
98
+ }
99
+ if (seo.description) {
100
+ tags.addMeta('twitter:description', seo.description);
101
+ }
102
+ if (seo.image) {
103
+ tags.addMeta('twitter:image', seo.image);
104
+ }
105
+ return { html: nextHtml, tags: tags.list() };
106
+ }
107
+ catch {
108
+ return html;
109
+ }
110
+ },
111
+ };
112
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ export default function serverConfigPlugin(): Plugin;
3
+ //# sourceMappingURL=server-config.d.ts.map
@@ -0,0 +1,36 @@
1
+ const PREVIEW_ORIGINS_ENV = 'FIMO_PREVIEW_ALLOWED_ORIGINS';
2
+ function readPreviewHosts() {
3
+ return (process.env[PREVIEW_ORIGINS_ENV] ?? '')
4
+ .split(',')
5
+ .map((origin) => origin.trim())
6
+ .filter(Boolean)
7
+ .map((origin) => (origin.startsWith('**.') ? `.${origin.slice(3)}` : origin));
8
+ }
9
+ export default function serverConfigPlugin() {
10
+ return {
11
+ name: 'fimo:server-config',
12
+ config(config) {
13
+ const configuredHosts = config.server?.allowedHosts;
14
+ const previewHosts = readPreviewHosts();
15
+ const allowedHosts = configuredHosts === true
16
+ ? true
17
+ : [...(configuredHosts ?? []), ...previewHosts].filter((host, index, hosts) => hosts.indexOf(host) === index);
18
+ return {
19
+ server: {
20
+ ...(allowedHosts === true || allowedHosts.length > 0 ? { allowedHosts } : {}),
21
+ hmr: {
22
+ overlay: false,
23
+ },
24
+ watch: {
25
+ ignored: ['**/.worktrees/**', '**/.fimo/config.json', '**/fimo-config.json'],
26
+ },
27
+ },
28
+ build: {},
29
+ optimizeDeps: {
30
+ include: [],
31
+ exclude: [],
32
+ },
33
+ };
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { Plugin } from 'vite';
2
+ export default function sitemapPlugin(): Plugin;
3
+ //# sourceMappingURL=sitemap.d.ts.map