fumapress 0.0.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/LICENSE +21 -0
- package/dist/chunk-KMVUUT6X.js +10 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +97 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +13 -0
- package/dist/vite.d.ts +5 -0
- package/dist/vite.js +124 -0
- package/package.json +62 -0
- package/src/cli/index.ts +18 -0
- package/src/config/content.ts +7 -0
- package/src/config/global.ts +16 -0
- package/src/config/routes.ts +29 -0
- package/src/constants.ts +7 -0
- package/src/entry.browser.tsx +52 -0
- package/src/entry.rsc.tsx +46 -0
- package/src/entry.ssr.tsx +38 -0
- package/src/index.ts +2 -0
- package/src/lib/find-file.ts +12 -0
- package/src/lib/get-config.ts +42 -0
- package/src/lib/layout.shared.tsx +9 -0
- package/src/lib/root-layout.tsx +26 -0
- package/src/lib/source.ts +35 -0
- package/src/routes/config.ts +28 -0
- package/src/routes/docs/page.tsx +36 -0
- package/src/routes/docs/search.ts +11 -0
- package/src/routes/home.tsx +24 -0
- package/src/routes/root/client.tsx +50 -0
- package/src/routes/root/index.tsx +12 -0
- package/src/vite/build.ts +69 -0
- package/src/vite/dev.ts +41 -0
- package/src/vite/index.ts +62 -0
- package/src/vite-env.d.ts +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Fuma
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "../chunk-MLKGABMK.js";
|
|
3
|
+
|
|
4
|
+
// src/cli/index.ts
|
|
5
|
+
import { program } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/vite/build.ts
|
|
8
|
+
import { createBuilder } from "vite";
|
|
9
|
+
async function viteBuild({
|
|
10
|
+
assetsInlineLimit,
|
|
11
|
+
clearScreen,
|
|
12
|
+
config: configFile,
|
|
13
|
+
emptyOutDir,
|
|
14
|
+
force,
|
|
15
|
+
logLevel,
|
|
16
|
+
minify,
|
|
17
|
+
mode,
|
|
18
|
+
sourcemapClient,
|
|
19
|
+
sourcemapServer
|
|
20
|
+
} = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const builder = await createBuilder({
|
|
23
|
+
mode,
|
|
24
|
+
configFile,
|
|
25
|
+
build: {
|
|
26
|
+
assetsInlineLimit,
|
|
27
|
+
emptyOutDir,
|
|
28
|
+
minify
|
|
29
|
+
},
|
|
30
|
+
optimizeDeps: { force },
|
|
31
|
+
clearScreen,
|
|
32
|
+
logLevel,
|
|
33
|
+
plugins: [
|
|
34
|
+
{
|
|
35
|
+
name: "fumapress:cli-config",
|
|
36
|
+
configEnvironment(name) {
|
|
37
|
+
if (sourcemapClient && name === "client") {
|
|
38
|
+
return {
|
|
39
|
+
build: {
|
|
40
|
+
sourcemap: sourcemapClient
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (sourcemapServer && name !== "client") {
|
|
45
|
+
return {
|
|
46
|
+
build: {
|
|
47
|
+
sourcemap: sourcemapServer
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
await builder.buildApp();
|
|
56
|
+
console.log("Build completed successfully");
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("Build failed:", error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/vite/dev.ts
|
|
63
|
+
import { createServer } from "vite";
|
|
64
|
+
async function dev({
|
|
65
|
+
clearScreen,
|
|
66
|
+
config: configFile,
|
|
67
|
+
cors,
|
|
68
|
+
force,
|
|
69
|
+
host,
|
|
70
|
+
logLevel,
|
|
71
|
+
mode,
|
|
72
|
+
open,
|
|
73
|
+
port,
|
|
74
|
+
strictPort
|
|
75
|
+
} = {}) {
|
|
76
|
+
const server = await createServer({
|
|
77
|
+
mode,
|
|
78
|
+
configFile,
|
|
79
|
+
server: { open, cors, host, port, strictPort },
|
|
80
|
+
optimizeDeps: { force },
|
|
81
|
+
clearScreen,
|
|
82
|
+
logLevel
|
|
83
|
+
});
|
|
84
|
+
await server.listen();
|
|
85
|
+
server.printUrls();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/cli/index.ts
|
|
89
|
+
program.command("build").action(async () => {
|
|
90
|
+
await viteBuild();
|
|
91
|
+
});
|
|
92
|
+
program.command("dev").action(async () => {
|
|
93
|
+
await dev();
|
|
94
|
+
});
|
|
95
|
+
program.command("typegen").action(async () => {
|
|
96
|
+
});
|
|
97
|
+
void program.parseAsync(process.argv);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { unstable_RSCRouteConfigEntry } from 'react-router';
|
|
2
|
+
|
|
3
|
+
type RouteConfigParentEntry = Exclude<unstable_RSCRouteConfigEntry, {
|
|
4
|
+
index: true;
|
|
5
|
+
}>;
|
|
6
|
+
interface RoutesConfig {
|
|
7
|
+
/**
|
|
8
|
+
* the root entry
|
|
9
|
+
*/
|
|
10
|
+
root?: RouteConfigParentEntry;
|
|
11
|
+
/**
|
|
12
|
+
* Layout name -> layout entry
|
|
13
|
+
*/
|
|
14
|
+
layouts?: Record<string, RouteConfigParentEntry>;
|
|
15
|
+
/**
|
|
16
|
+
* entry for content pages
|
|
17
|
+
*/
|
|
18
|
+
page?: unstable_RSCRouteConfigEntry;
|
|
19
|
+
/**
|
|
20
|
+
* Add additional entries
|
|
21
|
+
*/
|
|
22
|
+
extends?: unstable_RSCRouteConfigEntry;
|
|
23
|
+
}
|
|
24
|
+
declare function defineRoutes(config: RoutesConfig): RoutesConfig;
|
|
25
|
+
|
|
26
|
+
interface FumapressConfig {
|
|
27
|
+
/**
|
|
28
|
+
* the directory for app files (relative to project root)
|
|
29
|
+
*
|
|
30
|
+
* @defaultValue './app'
|
|
31
|
+
*/
|
|
32
|
+
appDir: string;
|
|
33
|
+
}
|
|
34
|
+
declare function defineConfig(config: Partial<FumapressConfig>): FumapressConfig;
|
|
35
|
+
|
|
36
|
+
export { type FumapressConfig, type RoutesConfig, defineConfig, defineRoutes };
|
package/dist/index.js
ADDED
package/dist/vite.d.ts
ADDED
package/dist/vite.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineConfig
|
|
3
|
+
} from "./chunk-KMVUUT6X.js";
|
|
4
|
+
import {
|
|
5
|
+
__export
|
|
6
|
+
} from "./chunk-MLKGABMK.js";
|
|
7
|
+
|
|
8
|
+
// src/vite/index.ts
|
|
9
|
+
import react from "@vitejs/plugin-react";
|
|
10
|
+
import rsc from "@vitejs/plugin-rsc";
|
|
11
|
+
import mdx from "fumadocs-mdx/vite";
|
|
12
|
+
import path2 from "path";
|
|
13
|
+
|
|
14
|
+
// src/config/content.ts
|
|
15
|
+
var content_exports = {};
|
|
16
|
+
__export(content_exports, {
|
|
17
|
+
default: () => content_default,
|
|
18
|
+
docs: () => docs
|
|
19
|
+
});
|
|
20
|
+
import { defineConfig as defineConfig2, defineDocs } from "fumadocs-mdx/config";
|
|
21
|
+
var docs = defineDocs({
|
|
22
|
+
dir: "content"
|
|
23
|
+
});
|
|
24
|
+
var content_default = defineConfig2();
|
|
25
|
+
|
|
26
|
+
// src/constants.ts
|
|
27
|
+
import { fileURLToPath } from "url";
|
|
28
|
+
import path from "path";
|
|
29
|
+
var baseDir = path.join(
|
|
30
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
31
|
+
"../"
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// src/lib/get-config.ts
|
|
35
|
+
import { glob } from "tinyglobby";
|
|
36
|
+
var DefaultConfig = defineConfig({});
|
|
37
|
+
var DefaultConfigPatterns = ["fumapress.config.{js,jsx,ts,tsx,md,mdx}"];
|
|
38
|
+
async function findConfigPath() {
|
|
39
|
+
const paths = await glob(DefaultConfigPatterns);
|
|
40
|
+
return paths.length > 0 ? paths[0] : null;
|
|
41
|
+
}
|
|
42
|
+
async function loadConfig(configPath) {
|
|
43
|
+
if (configPath === null) return DefaultConfig;
|
|
44
|
+
if (configPath === void 0) return loadConfig(await findConfigPath());
|
|
45
|
+
try {
|
|
46
|
+
const { default: userConfig } = await import(configPath);
|
|
47
|
+
return checkConfig(userConfig);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(
|
|
50
|
+
`Failed to load config from ${configPath}:`,
|
|
51
|
+
error instanceof Error ? error.message : String(error)
|
|
52
|
+
);
|
|
53
|
+
return DefaultConfig;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function checkConfig(loaded) {
|
|
57
|
+
if (typeof loaded !== "object" || loaded === null) {
|
|
58
|
+
throw new Error(`Config file ${loaded} must export an object.`);
|
|
59
|
+
}
|
|
60
|
+
return loaded;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/lib/find-file.ts
|
|
64
|
+
import { access } from "fs/promises";
|
|
65
|
+
async function findFile(paths) {
|
|
66
|
+
for (const path3 of paths) {
|
|
67
|
+
try {
|
|
68
|
+
await access(path3);
|
|
69
|
+
return path3;
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/vite/index.ts
|
|
77
|
+
var mdxConfigPath = path2.join(baseDir, "src/config/fumadocs-mdx.ts");
|
|
78
|
+
var extnames = [".js", ".ts", ".jsx", ".tsx"];
|
|
79
|
+
function fumapress() {
|
|
80
|
+
return [
|
|
81
|
+
resolveDir(),
|
|
82
|
+
react(),
|
|
83
|
+
rsc({
|
|
84
|
+
entries: {
|
|
85
|
+
client: path2.join(baseDir, "src/entry.browser.tsx"),
|
|
86
|
+
rsc: path2.join(baseDir, "src/entry.rsc.tsx"),
|
|
87
|
+
ssr: path2.join(baseDir, "src/entry.ssr.tsx")
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
mdx(content_exports, { configPath: mdxConfigPath, generateIndexFile: false })
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
function resolveDir() {
|
|
94
|
+
let configPath;
|
|
95
|
+
let config;
|
|
96
|
+
return {
|
|
97
|
+
name: "fumapress/resolve-dir",
|
|
98
|
+
enforce: "pre",
|
|
99
|
+
async configResolved() {
|
|
100
|
+
configPath = await findConfigPath();
|
|
101
|
+
config = await loadConfig(configPath);
|
|
102
|
+
},
|
|
103
|
+
resolveId(id) {
|
|
104
|
+
const appDir = "virtual:app/";
|
|
105
|
+
if (id.startsWith(appDir)) {
|
|
106
|
+
const name = path2.join(config.appDir, id.slice(appDir.length));
|
|
107
|
+
return findFile(extnames.map((ext) => name + ext));
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
configureServer(server) {
|
|
111
|
+
server.watcher.on("change", async (file) => {
|
|
112
|
+
if (!configPath) return;
|
|
113
|
+
const fullConfigPath = path2.resolve(configPath);
|
|
114
|
+
if (fullConfigPath === file) {
|
|
115
|
+
console.log(`Config changed: ${file}. Restarting dev server.`);
|
|
116
|
+
await server.restart();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
fumapress
|
|
124
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fumapress",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The minimal setup for Fumadocs",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Fumadocs",
|
|
7
|
+
"Docs"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://fumadocs.dev",
|
|
10
|
+
"repository": "github:fuma-nama/fumadocs",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Fuma Nama",
|
|
13
|
+
"type": "module",
|
|
14
|
+
"files": [
|
|
15
|
+
"src/*",
|
|
16
|
+
"dist/*"
|
|
17
|
+
],
|
|
18
|
+
"bin": {
|
|
19
|
+
"fumapress": "./dist/cli/index.js"
|
|
20
|
+
},
|
|
21
|
+
"main": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@vitejs/plugin-react": "^5.0.3",
|
|
25
|
+
"@vitejs/plugin-rsc": "^0.4.32",
|
|
26
|
+
"commander": "^14.0.1",
|
|
27
|
+
"picocolors": "^1.1.1",
|
|
28
|
+
"react": "^19.1.1",
|
|
29
|
+
"react-dom": "^19.1.1",
|
|
30
|
+
"react-router": "^7.9.3",
|
|
31
|
+
"tinyglobby": "^0.2.15",
|
|
32
|
+
"vite": "^7.1.7",
|
|
33
|
+
"fumadocs-core": "15.8.2",
|
|
34
|
+
"fumadocs-mdx": "12.0.1",
|
|
35
|
+
"fumadocs-ui": "15.8.2"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/mdx": "^2.0.13",
|
|
39
|
+
"@types/node": "^24.5.2",
|
|
40
|
+
"@types/react": "^19.1.14",
|
|
41
|
+
"@types/react-dom": "^19.1.9",
|
|
42
|
+
"typescript": "^5.9.2",
|
|
43
|
+
"eslint-config-custom": "0.0.0",
|
|
44
|
+
"tsconfig": "0.0.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@types/react": "*"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@types/react": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"dev": "tsup --watch",
|
|
60
|
+
"typecheck": "tsc --noEmit"
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { viteBuild } from '../vite/build';
|
|
4
|
+
import { dev } from '../vite/dev';
|
|
5
|
+
|
|
6
|
+
program.command('build').action(async () => {
|
|
7
|
+
await viteBuild();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
program.command('dev').action(async () => {
|
|
11
|
+
await dev();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
program.command('typegen').action(async () => {
|
|
15
|
+
// TODO: typegen content shapes
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
void program.parseAsync(process.argv);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface FumapressConfig {
|
|
2
|
+
/**
|
|
3
|
+
* the directory for app files (relative to project root)
|
|
4
|
+
*
|
|
5
|
+
* @defaultValue './app'
|
|
6
|
+
*/
|
|
7
|
+
appDir: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function defineConfig(
|
|
11
|
+
config: Partial<FumapressConfig>,
|
|
12
|
+
): FumapressConfig {
|
|
13
|
+
return {
|
|
14
|
+
appDir: config.appDir ?? './app',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { unstable_RSCRouteConfigEntry as RSCRouteConfigEntry } from 'react-router';
|
|
2
|
+
|
|
3
|
+
type RouteConfigParentEntry = Exclude<RSCRouteConfigEntry, { index: true }>;
|
|
4
|
+
|
|
5
|
+
export interface RoutesConfig {
|
|
6
|
+
/**
|
|
7
|
+
* the root entry
|
|
8
|
+
*/
|
|
9
|
+
root?: RouteConfigParentEntry;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Layout name -> layout entry
|
|
13
|
+
*/
|
|
14
|
+
layouts?: Record<string, RouteConfigParentEntry>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* entry for content pages
|
|
18
|
+
*/
|
|
19
|
+
page?: RSCRouteConfigEntry;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Add additional entries
|
|
23
|
+
*/
|
|
24
|
+
extends?: RSCRouteConfigEntry;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function defineRoutes(config: RoutesConfig): RoutesConfig {
|
|
28
|
+
return config;
|
|
29
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFromReadableStream,
|
|
3
|
+
createTemporaryReferenceSet,
|
|
4
|
+
encodeReply,
|
|
5
|
+
setServerCallback,
|
|
6
|
+
} from '@vitejs/plugin-rsc/browser';
|
|
7
|
+
import { startTransition, StrictMode } from 'react';
|
|
8
|
+
import { hydrateRoot } from 'react-dom/client';
|
|
9
|
+
import {
|
|
10
|
+
type DataRouter,
|
|
11
|
+
unstable_createCallServer as createCallServer,
|
|
12
|
+
unstable_getRSCStream as getRSCStream,
|
|
13
|
+
unstable_RSCHydratedRouter as RSCHydratedRouter,
|
|
14
|
+
type unstable_RSCPayload as RSCServerPayload,
|
|
15
|
+
} from 'react-router';
|
|
16
|
+
|
|
17
|
+
// Create and set the callServer function to support post-hydration server actions.
|
|
18
|
+
setServerCallback(
|
|
19
|
+
createCallServer({
|
|
20
|
+
createFromReadableStream,
|
|
21
|
+
createTemporaryReferenceSet,
|
|
22
|
+
encodeReply,
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Get and decode the initial server payload
|
|
27
|
+
createFromReadableStream<RSCServerPayload>(getRSCStream()).then((payload) => {
|
|
28
|
+
startTransition(async () => {
|
|
29
|
+
const formState =
|
|
30
|
+
payload.type === 'render' ? await payload.formState : undefined;
|
|
31
|
+
|
|
32
|
+
hydrateRoot(
|
|
33
|
+
document,
|
|
34
|
+
<StrictMode>
|
|
35
|
+
<RSCHydratedRouter
|
|
36
|
+
createFromReadableStream={createFromReadableStream}
|
|
37
|
+
payload={payload}
|
|
38
|
+
/>
|
|
39
|
+
</StrictMode>,
|
|
40
|
+
{
|
|
41
|
+
// @ts-expect-error - no types for this yet
|
|
42
|
+
formState,
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (import.meta.hot) {
|
|
49
|
+
import.meta.hot.on('rsc:update', () => {
|
|
50
|
+
(window as unknown as { __router: DataRouter }).__router.revalidate();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTemporaryReferenceSet,
|
|
3
|
+
decodeAction,
|
|
4
|
+
decodeFormState,
|
|
5
|
+
decodeReply,
|
|
6
|
+
loadServerAction,
|
|
7
|
+
renderToReadableStream,
|
|
8
|
+
} from '@vitejs/plugin-rsc/rsc';
|
|
9
|
+
import { unstable_matchRSCServerRequest as matchRSCServerRequest } from 'react-router';
|
|
10
|
+
|
|
11
|
+
import { routes } from './routes/config';
|
|
12
|
+
|
|
13
|
+
async function fetchServer(request: Request) {
|
|
14
|
+
return matchRSCServerRequest({
|
|
15
|
+
// Provide the React Server touchpoints.
|
|
16
|
+
createTemporaryReferenceSet,
|
|
17
|
+
decodeAction,
|
|
18
|
+
decodeFormState,
|
|
19
|
+
decodeReply,
|
|
20
|
+
loadServerAction,
|
|
21
|
+
// The incoming request.
|
|
22
|
+
request,
|
|
23
|
+
// The app routes.
|
|
24
|
+
routes: await routes(),
|
|
25
|
+
// Encode the match with the React Server implementation.
|
|
26
|
+
generateResponse(match, options) {
|
|
27
|
+
return new Response(renderToReadableStream(match.payload, options), {
|
|
28
|
+
status: match.statusCode,
|
|
29
|
+
headers: match.headers,
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default async function handler(request: Request) {
|
|
36
|
+
// Import the generateHTML function from the client environment
|
|
37
|
+
const ssr = await import.meta.viteRsc.loadModule<
|
|
38
|
+
typeof import('./entry.ssr')
|
|
39
|
+
>('ssr', 'index');
|
|
40
|
+
|
|
41
|
+
return ssr.generateHTML(request, fetchServer);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (import.meta.hot) {
|
|
45
|
+
import.meta.hot.accept();
|
|
46
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createFromReadableStream } from '@vitejs/plugin-rsc/ssr';
|
|
2
|
+
import { renderToReadableStream as renderHTMLToReadableStream } from 'react-dom/server.edge';
|
|
3
|
+
import {
|
|
4
|
+
unstable_routeRSCServerRequest as routeRSCServerRequest,
|
|
5
|
+
unstable_RSCStaticRouter as RSCStaticRouter,
|
|
6
|
+
} from 'react-router';
|
|
7
|
+
|
|
8
|
+
export async function generateHTML(
|
|
9
|
+
request: Request,
|
|
10
|
+
fetchServer: (request: Request) => Promise<Response>,
|
|
11
|
+
): Promise<Response> {
|
|
12
|
+
return await routeRSCServerRequest({
|
|
13
|
+
// The incoming request.
|
|
14
|
+
request,
|
|
15
|
+
// How to call the React Server.
|
|
16
|
+
fetchServer,
|
|
17
|
+
// Provide the React Server touchpoints.
|
|
18
|
+
createFromReadableStream,
|
|
19
|
+
// Render the router to HTML.
|
|
20
|
+
async renderHTML(getPayload) {
|
|
21
|
+
const payload = await getPayload();
|
|
22
|
+
const formState =
|
|
23
|
+
payload.type === 'render' ? await payload.formState : undefined;
|
|
24
|
+
|
|
25
|
+
const bootstrapScriptContent =
|
|
26
|
+
await import.meta.viteRsc.loadBootstrapScriptContent('index');
|
|
27
|
+
|
|
28
|
+
return await renderHTMLToReadableStream(
|
|
29
|
+
<RSCStaticRouter getPayload={getPayload} />,
|
|
30
|
+
{
|
|
31
|
+
bootstrapScriptContent,
|
|
32
|
+
// @ts-expect-error - no types for this yet
|
|
33
|
+
formState,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineConfig, type FumapressConfig } from '../config/global';
|
|
2
|
+
import { glob } from 'tinyglobby';
|
|
3
|
+
|
|
4
|
+
const DefaultConfig = defineConfig({});
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default glob patterns for finding config file
|
|
8
|
+
*/
|
|
9
|
+
const DefaultConfigPatterns = ['fumapress.config.{js,jsx,ts,tsx,md,mdx}'];
|
|
10
|
+
|
|
11
|
+
export async function findConfigPath(): Promise<string | null> {
|
|
12
|
+
const paths = await glob(DefaultConfigPatterns);
|
|
13
|
+
|
|
14
|
+
return paths.length > 0 ? paths[0] : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadConfig(
|
|
18
|
+
configPath?: string | null,
|
|
19
|
+
): Promise<FumapressConfig> {
|
|
20
|
+
if (configPath === null) return DefaultConfig;
|
|
21
|
+
if (configPath === undefined) return loadConfig(await findConfigPath());
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const { default: userConfig } = await import(configPath);
|
|
25
|
+
|
|
26
|
+
return checkConfig(userConfig);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(
|
|
29
|
+
`Failed to load config from ${configPath}:`,
|
|
30
|
+
error instanceof Error ? error.message : String(error),
|
|
31
|
+
);
|
|
32
|
+
return DefaultConfig;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkConfig(loaded: unknown): FumapressConfig {
|
|
37
|
+
if (typeof loaded !== 'object' || loaded === null) {
|
|
38
|
+
throw new Error(`Config file ${loaded} must export an object.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return loaded as FumapressConfig;
|
|
42
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { RootProvider } from 'fumadocs-ui/provider/react-router';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
|
|
5
|
+
|
|
6
|
+
export function Layout({ children }: { children: ReactNode }) {
|
|
7
|
+
return (
|
|
8
|
+
<html lang="en" suppressHydrationWarning>
|
|
9
|
+
<head>
|
|
10
|
+
<meta charSet="utf-8" />
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
12
|
+
<Meta />
|
|
13
|
+
<Links />
|
|
14
|
+
</head>
|
|
15
|
+
<body className="flex flex-col min-h-screen">
|
|
16
|
+
<RootProvider>{children}</RootProvider>
|
|
17
|
+
<ScrollRestoration />
|
|
18
|
+
<Scripts />
|
|
19
|
+
</body>
|
|
20
|
+
</html>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function App() {
|
|
25
|
+
return <Outlet />;
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/// <reference types="fumadocs-mdx" />
|
|
2
|
+
import { loader } from 'fumadocs-core/source';
|
|
3
|
+
import { fromConfig } from 'fumadocs-mdx/runtime/vite';
|
|
4
|
+
import type * as Config from '../config/content';
|
|
5
|
+
|
|
6
|
+
export const create = fromConfig<typeof Config>();
|
|
7
|
+
|
|
8
|
+
export const docs = {
|
|
9
|
+
doc: create.doc(
|
|
10
|
+
'docs',
|
|
11
|
+
'./content',
|
|
12
|
+
import.meta.glob(['./**/*.{mdx,md}'], {
|
|
13
|
+
base: '/content',
|
|
14
|
+
query: {
|
|
15
|
+
collection: 'docs',
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
meta: create.meta(
|
|
20
|
+
'docs',
|
|
21
|
+
'./content',
|
|
22
|
+
import.meta.glob(['./**/*.{json,yaml}'], {
|
|
23
|
+
import: 'default',
|
|
24
|
+
base: '/content',
|
|
25
|
+
query: {
|
|
26
|
+
collection: 'docs',
|
|
27
|
+
},
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const source = loader({
|
|
33
|
+
source: await create.sourceAsync(docs.doc, docs.meta),
|
|
34
|
+
baseUrl: '/docs',
|
|
35
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { unstable_RSCRouteConfig as RSCRouteConfig } from 'react-router';
|
|
2
|
+
|
|
3
|
+
export async function routes() {
|
|
4
|
+
const { default: config } = await import('virtual:app/routes');
|
|
5
|
+
const root = config.root ?? {
|
|
6
|
+
id: 'root',
|
|
7
|
+
path: '',
|
|
8
|
+
lazy: () => import('./root'),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
...root,
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
id: 'home',
|
|
17
|
+
index: true,
|
|
18
|
+
lazy: () => import('./home'),
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'docs/*',
|
|
22
|
+
path: 'docs/*',
|
|
23
|
+
lazy: () => import('./docs/page'),
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
] satisfies RSCRouteConfig;
|
|
28
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
|
2
|
+
import {
|
|
3
|
+
DocsBody,
|
|
4
|
+
DocsDescription,
|
|
5
|
+
DocsPage,
|
|
6
|
+
DocsTitle,
|
|
7
|
+
} from 'fumadocs-ui/page';
|
|
8
|
+
import { source } from '../../lib/source';
|
|
9
|
+
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
|
10
|
+
import { baseOptions } from '../../lib/layout.shared';
|
|
11
|
+
|
|
12
|
+
export default function ServerComponent({
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
params: Record<string, string>;
|
|
16
|
+
}) {
|
|
17
|
+
const slugs = params['*'].split('/').filter((v) => v.length > 0);
|
|
18
|
+
const page = source.getPage(slugs);
|
|
19
|
+
if (!page) throw new Response('Not found', { status: 404 });
|
|
20
|
+
|
|
21
|
+
const { body: MDX } = page.data;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<DocsLayout {...baseOptions()} tree={source.pageTree}>
|
|
25
|
+
<DocsPage toc={page.data.toc}>
|
|
26
|
+
<title>{page.data.title}</title>
|
|
27
|
+
<meta name="description" content={page.data.description} />
|
|
28
|
+
<DocsTitle>{page.data.title}</DocsTitle>
|
|
29
|
+
<DocsDescription>{page.data.description}</DocsDescription>
|
|
30
|
+
<DocsBody>
|
|
31
|
+
<MDX components={{ ...defaultMdxComponents }} />
|
|
32
|
+
</DocsBody>
|
|
33
|
+
</DocsPage>
|
|
34
|
+
</DocsLayout>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createFromSource } from 'fumadocs-core/search/server';
|
|
2
|
+
import type { LoaderFunctionArgs } from 'react-router';
|
|
3
|
+
import { source } from '../../lib/source';
|
|
4
|
+
|
|
5
|
+
const server = createFromSource(source, {
|
|
6
|
+
language: 'english',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export async function loader({ request }: LoaderFunctionArgs) {
|
|
10
|
+
return server.GET(request);
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
|
2
|
+
import { Link } from 'react-router';
|
|
3
|
+
import { baseOptions } from '../lib/layout.shared';
|
|
4
|
+
|
|
5
|
+
export default function ServerComponent() {
|
|
6
|
+
return (
|
|
7
|
+
<HomeLayout {...baseOptions()}>
|
|
8
|
+
<meta name="title" content="Fumapress" />
|
|
9
|
+
<meta name="description" content="Fumapress" />
|
|
10
|
+
<div className="p-4 flex flex-col items-center justify-center text-center flex-1">
|
|
11
|
+
<h1 className="text-xl font-bold mb-2">Fumadocs on React Router.</h1>
|
|
12
|
+
<p className="text-fd-muted-foreground mb-4">
|
|
13
|
+
The truly flexible docs framework on React.js.
|
|
14
|
+
</p>
|
|
15
|
+
<Link
|
|
16
|
+
className="text-sm bg-fd-primary text-fd-primary-foreground rounded-full font-medium px-4 py-2.5"
|
|
17
|
+
to="/docs"
|
|
18
|
+
>
|
|
19
|
+
Open Docs
|
|
20
|
+
</Link>
|
|
21
|
+
</div>
|
|
22
|
+
</HomeLayout>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isRouteErrorResponse,
|
|
5
|
+
Links,
|
|
6
|
+
Meta,
|
|
7
|
+
Scripts,
|
|
8
|
+
ScrollRestoration,
|
|
9
|
+
useRouteError,
|
|
10
|
+
} from 'react-router';
|
|
11
|
+
import { RootProvider } from 'fumadocs-ui/provider/react-router';
|
|
12
|
+
|
|
13
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en" suppressHydrationWarning>
|
|
16
|
+
<head>
|
|
17
|
+
<meta charSet="utf-8" />
|
|
18
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
19
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
20
|
+
<Meta />
|
|
21
|
+
<Links />
|
|
22
|
+
</head>
|
|
23
|
+
<body className="flex flex-col min-h-screen">
|
|
24
|
+
<RootProvider>{children}</RootProvider>
|
|
25
|
+
<ScrollRestoration />
|
|
26
|
+
<Scripts />
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ErrorBoundary() {
|
|
33
|
+
const error = useRouteError();
|
|
34
|
+
let status = 500;
|
|
35
|
+
let message = 'An unexpected error occurred.';
|
|
36
|
+
|
|
37
|
+
if (isRouteErrorResponse(error)) {
|
|
38
|
+
status = error.status;
|
|
39
|
+
message = status === 404 ? 'Page not found.' : error.statusText || message;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<main className="mx-auto max-w-screen-xl px-4 py-8 lg:py-12">
|
|
44
|
+
<article className="prose mx-auto">
|
|
45
|
+
<h1>{status}</h1>
|
|
46
|
+
<p>{message}</p>
|
|
47
|
+
</article>
|
|
48
|
+
</main>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Outlet } from 'react-router';
|
|
2
|
+
import { Layout as ClientLayout } from './client';
|
|
3
|
+
|
|
4
|
+
export { ErrorBoundary } from './client';
|
|
5
|
+
|
|
6
|
+
export function Layout({ children }: { children: React.ReactNode }) {
|
|
7
|
+
return <ClientLayout>{children}</ClientLayout>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function Component() {
|
|
11
|
+
return <Outlet />;
|
|
12
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { BuildOptions, createBuilder, LogLevel } from 'vite';
|
|
2
|
+
|
|
3
|
+
export interface ViteBuildOptions {
|
|
4
|
+
assetsInlineLimit?: number;
|
|
5
|
+
clearScreen?: boolean;
|
|
6
|
+
config?: string;
|
|
7
|
+
emptyOutDir?: boolean;
|
|
8
|
+
force?: boolean;
|
|
9
|
+
logLevel?: LogLevel;
|
|
10
|
+
minify?: BuildOptions['minify'];
|
|
11
|
+
mode?: string;
|
|
12
|
+
profile?: boolean;
|
|
13
|
+
sourcemapClient?: boolean | 'inline' | 'hidden';
|
|
14
|
+
sourcemapServer?: boolean | 'inline' | 'hidden';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function viteBuild({
|
|
18
|
+
assetsInlineLimit,
|
|
19
|
+
clearScreen,
|
|
20
|
+
config: configFile,
|
|
21
|
+
emptyOutDir,
|
|
22
|
+
force,
|
|
23
|
+
logLevel,
|
|
24
|
+
minify,
|
|
25
|
+
mode,
|
|
26
|
+
sourcemapClient,
|
|
27
|
+
sourcemapServer,
|
|
28
|
+
}: ViteBuildOptions = {}) {
|
|
29
|
+
try {
|
|
30
|
+
const builder = await createBuilder({
|
|
31
|
+
mode,
|
|
32
|
+
configFile,
|
|
33
|
+
build: {
|
|
34
|
+
assetsInlineLimit,
|
|
35
|
+
emptyOutDir,
|
|
36
|
+
minify,
|
|
37
|
+
},
|
|
38
|
+
optimizeDeps: { force },
|
|
39
|
+
clearScreen,
|
|
40
|
+
logLevel,
|
|
41
|
+
plugins: [
|
|
42
|
+
{
|
|
43
|
+
name: 'fumapress:cli-config',
|
|
44
|
+
configEnvironment(name) {
|
|
45
|
+
if (sourcemapClient && name === 'client') {
|
|
46
|
+
return {
|
|
47
|
+
build: {
|
|
48
|
+
sourcemap: sourcemapClient,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (sourcemapServer && name !== 'client') {
|
|
54
|
+
return {
|
|
55
|
+
build: {
|
|
56
|
+
sourcemap: sourcemapServer,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
await builder.buildApp();
|
|
65
|
+
console.log('Build completed successfully');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Build failed:', error);
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/vite/dev.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type * as Vite from 'vite';
|
|
2
|
+
import { createServer } from 'vite';
|
|
3
|
+
|
|
4
|
+
export interface ViteDevOptions {
|
|
5
|
+
clearScreen?: boolean;
|
|
6
|
+
config?: string;
|
|
7
|
+
cors?: boolean;
|
|
8
|
+
force?: boolean;
|
|
9
|
+
host?: boolean | string;
|
|
10
|
+
logLevel?: Vite.LogLevel;
|
|
11
|
+
mode?: string;
|
|
12
|
+
open?: boolean | string;
|
|
13
|
+
port?: number;
|
|
14
|
+
strictPort?: boolean;
|
|
15
|
+
profile?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function dev({
|
|
19
|
+
clearScreen,
|
|
20
|
+
config: configFile,
|
|
21
|
+
cors,
|
|
22
|
+
force,
|
|
23
|
+
host,
|
|
24
|
+
logLevel,
|
|
25
|
+
mode,
|
|
26
|
+
open,
|
|
27
|
+
port,
|
|
28
|
+
strictPort,
|
|
29
|
+
}: ViteDevOptions = {}) {
|
|
30
|
+
const server = await createServer({
|
|
31
|
+
mode,
|
|
32
|
+
configFile,
|
|
33
|
+
server: { open, cors, host, port, strictPort },
|
|
34
|
+
optimizeDeps: { force },
|
|
35
|
+
clearScreen,
|
|
36
|
+
logLevel,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await server.listen();
|
|
40
|
+
server.printUrls();
|
|
41
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Plugin, PluginOption } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import rsc from '@vitejs/plugin-rsc';
|
|
4
|
+
import mdx from 'fumadocs-mdx/vite';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import * as MdxConfig from '../config/content';
|
|
7
|
+
import { baseDir } from '../constants';
|
|
8
|
+
import { findConfigPath, loadConfig } from '../lib/get-config';
|
|
9
|
+
import type { FumapressConfig } from '../config/global';
|
|
10
|
+
import { findFile } from '../lib/find-file';
|
|
11
|
+
|
|
12
|
+
const mdxConfigPath = path.join(baseDir, 'src/config/fumadocs-mdx.ts');
|
|
13
|
+
const extnames = ['.js', '.ts', '.jsx', '.tsx'];
|
|
14
|
+
|
|
15
|
+
export function fumapress(): PluginOption[] {
|
|
16
|
+
return [
|
|
17
|
+
resolveDir(),
|
|
18
|
+
react(),
|
|
19
|
+
rsc({
|
|
20
|
+
entries: {
|
|
21
|
+
client: path.join(baseDir, 'src/entry.browser.tsx'),
|
|
22
|
+
rsc: path.join(baseDir, 'src/entry.rsc.tsx'),
|
|
23
|
+
ssr: path.join(baseDir, 'src/entry.ssr.tsx'),
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
mdx(MdxConfig, { configPath: mdxConfigPath, generateIndexFile: false }),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveDir(): Plugin {
|
|
31
|
+
let configPath: string | null;
|
|
32
|
+
let config: FumapressConfig;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
name: 'fumapress/resolve-dir',
|
|
36
|
+
enforce: 'pre',
|
|
37
|
+
async configResolved() {
|
|
38
|
+
configPath = await findConfigPath();
|
|
39
|
+
config = await loadConfig(configPath);
|
|
40
|
+
},
|
|
41
|
+
resolveId(id) {
|
|
42
|
+
const appDir = 'virtual:app/';
|
|
43
|
+
|
|
44
|
+
if (id.startsWith(appDir)) {
|
|
45
|
+
const name = path.join(config.appDir, id.slice(appDir.length));
|
|
46
|
+
|
|
47
|
+
return findFile(extnames.map((ext) => name + ext));
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
configureServer(server) {
|
|
51
|
+
server.watcher.on('change', async (file) => {
|
|
52
|
+
if (!configPath) return;
|
|
53
|
+
|
|
54
|
+
const fullConfigPath = path.resolve(configPath);
|
|
55
|
+
if (fullConfigPath === file) {
|
|
56
|
+
console.log(`Config changed: ${file}. Restarting dev server.`);
|
|
57
|
+
await server.restart();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|