wiki-viewer 1.0.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/.next/standalone/.github/workflows/publish.yml +37 -0
- package/.next/standalone/DESIGN.md +521 -0
- package/.next/standalone/README.md +283 -0
- package/.next/standalone/bin/wiki-viewer.js +158 -0
- package/.next/standalone/next.config.ts +11 -0
- package/.next/standalone/package.json +120 -0
- package/.next/standalone/pnpm-lock.yaml +5654 -0
- package/.next/standalone/pnpm-workspace.yaml +2 -0
- package/.next/standalone/postcss.config.mjs +2 -0
- package/.next/standalone/public/logo.svg +11 -0
- package/.next/standalone/public/public/logo.svg +11 -0
- package/.next/standalone/server.js +44 -0
- package/.next/standalone/src/app/api/app-proxy/[...path]/route.ts +247 -0
- package/.next/standalone/src/app/api/assets/[...path]/route.ts +70 -0
- package/.next/standalone/src/app/api/system/browse/route.ts +78 -0
- package/.next/standalone/src/app/api/system/clear-root/route.ts +9 -0
- package/.next/standalone/src/app/api/system/config/route.ts +10 -0
- package/.next/standalone/src/app/api/system/pins/route.ts +31 -0
- package/.next/standalone/src/app/api/system/reveal/route.ts +28 -0
- package/.next/standalone/src/app/api/system/root-status/route.ts +9 -0
- package/.next/standalone/src/app/api/system/set-root/route.ts +23 -0
- package/.next/standalone/src/app/api/upload/[...path]/route.ts +80 -0
- package/.next/standalone/src/app/api/wiki/app/route.ts +41 -0
- package/.next/standalone/src/app/api/wiki/content/route.ts +60 -0
- package/.next/standalone/src/app/api/wiki/file/route.ts +52 -0
- package/.next/standalone/src/app/api/wiki/folder/route.ts +26 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +45 -0
- package/.next/standalone/src/app/api/wiki/page/route.ts +84 -0
- package/.next/standalone/src/app/api/wiki/route.ts +98 -0
- package/.next/standalone/src/app/api/wiki/slugs/route.ts +50 -0
- package/.next/standalone/src/app/api/wiki/upload/route.ts +87 -0
- package/.next/standalone/src/app/api/wiki/watch/route.ts +86 -0
- package/.next/standalone/src/app/globals.css +716 -0
- package/.next/standalone/src/app/icon.svg +11 -0
- package/.next/standalone/src/app/layout.tsx +35 -0
- package/.next/standalone/src/app/page.tsx +1509 -0
- package/.next/standalone/src/components/confirm-dialog.tsx +56 -0
- package/.next/standalone/src/components/dir-picker.tsx +393 -0
- package/.next/standalone/src/components/editor/bubble-menu.tsx +286 -0
- package/.next/standalone/src/components/editor/callout-extension.ts +36 -0
- package/.next/standalone/src/components/editor/csv-viewer.tsx +335 -0
- package/.next/standalone/src/components/editor/editor-toolbar.tsx +564 -0
- package/.next/standalone/src/components/editor/editor.tsx +633 -0
- package/.next/standalone/src/components/editor/extensions/drag-handle.ts +325 -0
- package/.next/standalone/src/components/editor/extensions/heading-anchors.ts +55 -0
- package/.next/standalone/src/components/editor/extensions/resizable-image.tsx +159 -0
- package/.next/standalone/src/components/editor/extensions.ts +148 -0
- package/.next/standalone/src/components/editor/file-fallback-viewer.tsx +74 -0
- package/.next/standalone/src/components/editor/folder-index.tsx +235 -0
- package/.next/standalone/src/components/editor/google-doc-viewer.tsx +73 -0
- package/.next/standalone/src/components/editor/image-viewer.tsx +56 -0
- package/.next/standalone/src/components/editor/link-popover.tsx +111 -0
- package/.next/standalone/src/components/editor/media-popover.tsx +211 -0
- package/.next/standalone/src/components/editor/media-viewer.tsx +69 -0
- package/.next/standalone/src/components/editor/mermaid-viewer.tsx +285 -0
- package/.next/standalone/src/components/editor/node-app-viewer.tsx +269 -0
- package/.next/standalone/src/components/editor/notebook-viewer.tsx +370 -0
- package/.next/standalone/src/components/editor/office/docx-viewer.tsx +92 -0
- package/.next/standalone/src/components/editor/office/office-chrome.tsx +82 -0
- package/.next/standalone/src/components/editor/office/pptx-viewer.tsx +103 -0
- package/.next/standalone/src/components/editor/office/xlsx-viewer.tsx +116 -0
- package/.next/standalone/src/components/editor/pdf-viewer.tsx +31 -0
- package/.next/standalone/src/components/editor/slash-commands.tsx +454 -0
- package/.next/standalone/src/components/editor/source-viewer.tsx +212 -0
- package/.next/standalone/src/components/editor/table-menu.tsx +232 -0
- package/.next/standalone/src/components/editor/website-viewer.tsx +74 -0
- package/.next/standalone/src/components/editor/wiki-link-create-dialog.tsx +188 -0
- package/.next/standalone/src/components/editor/wiki-link-decorator.ts +79 -0
- package/.next/standalone/src/components/editor/wiki-link-extension.ts +120 -0
- package/.next/standalone/src/components/editor/wiki-link-picker.tsx +250 -0
- package/.next/standalone/src/components/layout/viewer-toolbar.tsx +54 -0
- package/.next/standalone/src/components/theme-provider.tsx +16 -0
- package/.next/standalone/src/components/theme-toggle.tsx +24 -0
- package/.next/standalone/src/components/ui/badge.tsx +35 -0
- package/.next/standalone/src/components/ui/button.tsx +56 -0
- package/.next/standalone/src/components/ui/card.tsx +63 -0
- package/.next/standalone/src/components/ui/collapsible.tsx +9 -0
- package/.next/standalone/src/components/ui/command.tsx +150 -0
- package/.next/standalone/src/components/ui/context-menu.tsx +191 -0
- package/.next/standalone/src/components/ui/dialog.tsx +119 -0
- package/.next/standalone/src/components/ui/dropdown-menu.tsx +194 -0
- package/.next/standalone/src/components/ui/input.tsx +22 -0
- package/.next/standalone/src/components/ui/label.tsx +25 -0
- package/.next/standalone/src/components/ui/popover.tsx +33 -0
- package/.next/standalone/src/components/ui/scroll-area.tsx +47 -0
- package/.next/standalone/src/components/ui/select.tsx +156 -0
- package/.next/standalone/src/components/ui/separator.tsx +30 -0
- package/.next/standalone/src/components/ui/sheet.tsx +140 -0
- package/.next/standalone/src/components/ui/skeleton.tsx +15 -0
- package/.next/standalone/src/components/ui/switch.tsx +28 -0
- package/.next/standalone/src/components/ui/tabs.tsx +54 -0
- package/.next/standalone/src/components/ui/textarea.tsx +21 -0
- package/.next/standalone/src/components/ui/tip.tsx +23 -0
- package/.next/standalone/src/components/ui/toggle-group.tsx +41 -0
- package/.next/standalone/src/components/ui/tooltip.tsx +27 -0
- package/.next/standalone/src/components/wiki/frontmatter-header.tsx +164 -0
- package/.next/standalone/src/lib/app-runner.ts +231 -0
- package/.next/standalone/src/lib/cabinets/tree.ts +75 -0
- package/.next/standalone/src/lib/config.ts +32 -0
- package/.next/standalone/src/lib/embeds/detect.ts +142 -0
- package/.next/standalone/src/lib/google/detect.ts +107 -0
- package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +84 -0
- package/.next/standalone/src/lib/markdown/remark-wikilinks.ts +100 -0
- package/.next/standalone/src/lib/markdown/to-html.ts +168 -0
- package/.next/standalone/src/lib/markdown/to-markdown.ts +238 -0
- package/.next/standalone/src/lib/root-dir.ts +35 -0
- package/.next/standalone/src/lib/toast.ts +9 -0
- package/.next/standalone/src/lib/utils.ts +6 -0
- package/.next/standalone/src/lib/wiki-helpers.ts +36 -0
- package/.next/standalone/src/stores/ai-panel-store.ts +23 -0
- package/.next/standalone/src/stores/editor-store.ts +244 -0
- package/.next/standalone/src/stores/tree-store.ts +52 -0
- package/.next/standalone/src/stores/wiki-slugs-store.ts +74 -0
- package/.next/standalone/src/types/index.ts +47 -0
- package/.next/standalone/tailwind.config.ts +201 -0
- package/.next/standalone/tsconfig.json +41 -0
- package/README.md +283 -0
- package/bin/wiki-viewer.js +158 -0
- package/package.json +120 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<rect x="15" y="15" width="70" height="70" rx="8" fill="#292524"/>
|
|
3
|
+
|
|
4
|
+
<rect x="23" y="29" width="46" height="8" rx="2" fill="#ffffff"/>
|
|
5
|
+
<rect x="23" y="45" width="34" height="8" rx="2" fill="#ffffff"/>
|
|
6
|
+
|
|
7
|
+
<rect x="23" y="61" width="16" height="8" rx="2" fill="#ffffff"/>
|
|
8
|
+
<rect x="43" y="61" width="28" height="8" rx="2" fill="#ffffff"/>
|
|
9
|
+
|
|
10
|
+
<path d="M 69 57 L 78 65 L 69 73 Z" fill="#ffffff"/>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<rect x="15" y="15" width="70" height="70" rx="8" fill="#292524"/>
|
|
3
|
+
|
|
4
|
+
<rect x="23" y="29" width="46" height="8" rx="2" fill="#ffffff"/>
|
|
5
|
+
<rect x="23" y="45" width="34" height="8" rx="2" fill="#ffffff"/>
|
|
6
|
+
|
|
7
|
+
<rect x="23" y="61" width="16" height="8" rx="2" fill="#ffffff"/>
|
|
8
|
+
<rect x="43" y="61" width="28" height="8" rx="2" fill="#ffffff"/>
|
|
9
|
+
|
|
10
|
+
<path d="M 69 57 L 78 65 L 69 73 Z" fill="#ffffff"/>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
performance.mark('next-start');
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import module from 'node:module'
|
|
5
|
+
const require = module.createRequire(import.meta.url)
|
|
6
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const dir = path.join(__dirname)
|
|
10
|
+
|
|
11
|
+
process.env.NODE_ENV = 'production'
|
|
12
|
+
process.chdir(__dirname)
|
|
13
|
+
|
|
14
|
+
const currentPort = parseInt(process.env.PORT, 10) || 3000
|
|
15
|
+
const hostname = process.env.HOSTNAME || '0.0.0.0'
|
|
16
|
+
|
|
17
|
+
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
|
|
18
|
+
const nextConfig = {"env":{},"webpack":null,"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":false,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/sil/wiki-viewer","allowedDevOrigins":["devvm"],"cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","turbopack":{"root":"/home/sil/wiki-viewer"},"distDirRoot":".next"}
|
|
19
|
+
|
|
20
|
+
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
|
|
21
|
+
|
|
22
|
+
require('next')
|
|
23
|
+
const { startServer } = require('next/dist/server/lib/start-server')
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
Number.isNaN(keepAliveTimeout) ||
|
|
27
|
+
!Number.isFinite(keepAliveTimeout) ||
|
|
28
|
+
keepAliveTimeout < 0
|
|
29
|
+
) {
|
|
30
|
+
keepAliveTimeout = undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
startServer({
|
|
34
|
+
dir,
|
|
35
|
+
isDev: false,
|
|
36
|
+
config: nextConfig,
|
|
37
|
+
hostname,
|
|
38
|
+
port: currentPort,
|
|
39
|
+
allowRetry: false,
|
|
40
|
+
keepAliveTimeout,
|
|
41
|
+
}).catch((err) => {
|
|
42
|
+
console.error(err);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reverse proxy for node-app directories — built on undici.request()
|
|
3
|
+
*
|
|
4
|
+
* Unlike fetch(), undici.request() does NOT auto-decompress, so compressed
|
|
5
|
+
* assets (gzip/br) stream through with their Content-Encoding intact and the
|
|
6
|
+
* browser handles decompression itself. No ERR_CONTENT_DECODING_FAILED.
|
|
7
|
+
*
|
|
8
|
+
* For HTML/CSS (which we rewrite) we force accept-encoding:identity upstream
|
|
9
|
+
* so we always receive plain text we can safely manipulate.
|
|
10
|
+
*/
|
|
11
|
+
import { Readable } from "node:stream";
|
|
12
|
+
import { request as undiciRequest } from "undici";
|
|
13
|
+
import type { Dispatcher } from "undici";
|
|
14
|
+
import { NextResponse } from "next/server";
|
|
15
|
+
import { resolveByPrefix } from "@/lib/app-runner";
|
|
16
|
+
|
|
17
|
+
const HOP_BY_HOP = new Set([
|
|
18
|
+
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
|
|
19
|
+
"te", "trailers", "transfer-encoding", "upgrade",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// ── service worker ────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function makeServiceWorker(proxyBase: string): string {
|
|
25
|
+
return `
|
|
26
|
+
/* wiki-viewer injected service worker */
|
|
27
|
+
const BASE = ${JSON.stringify(proxyBase)};
|
|
28
|
+
self.addEventListener("install", () => self.skipWaiting());
|
|
29
|
+
self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
|
|
30
|
+
self.addEventListener("fetch", (event) => {
|
|
31
|
+
const url = new URL(event.request.url);
|
|
32
|
+
if (url.origin !== self.location.origin) return;
|
|
33
|
+
if (url.pathname.startsWith(BASE + "/")) return;
|
|
34
|
+
if (url.pathname === BASE + "/sw-proxy.js") return;
|
|
35
|
+
event.respondWith(fetch(BASE + url.pathname + url.search, {
|
|
36
|
+
method: event.request.method,
|
|
37
|
+
headers: event.request.headers,
|
|
38
|
+
body: ["GET","HEAD"].includes(event.request.method) ? undefined : event.request.body,
|
|
39
|
+
credentials: event.request.credentials,
|
|
40
|
+
}));
|
|
41
|
+
});`.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── rewriters ─────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function rewriteHtml(html: string, proxyBase: string): string {
|
|
47
|
+
let out = html;
|
|
48
|
+
out = out.replace(/((?:src|href|action|data-src|data-href|content)=")\/(?!\/)/g, `$1${proxyBase}/`);
|
|
49
|
+
out = out.replace(/(srcset="[^"]*)\/(?!\/)/g, `$1${proxyBase}/`);
|
|
50
|
+
out = out.replace(/(<head(?:\s[^>]*)?>)/i, `$1\n<base href="${proxyBase}/">`);
|
|
51
|
+
// Inject before </head>. Works without service workers (non-localhost HTTP):
|
|
52
|
+
//
|
|
53
|
+
// 1. replaceState — strips the proxy prefix from the initial URL so
|
|
54
|
+
// BrowserRouter sees "/" instead of "/api/app-proxy/apps/X"
|
|
55
|
+
// 2. fetch/XHR overrides — rewrite absolute-path calls (/api/...) through
|
|
56
|
+
// the proxy so they reach the upstream app, not wiki-viewer
|
|
57
|
+
// 3. SW — best-effort (only works on localhost/HTTPS), provides navigation
|
|
58
|
+
// interception for hard-refreshes on sub-routes
|
|
59
|
+
const patches = `<script>
|
|
60
|
+
(function(){
|
|
61
|
+
var BASE = ${JSON.stringify(proxyBase)};
|
|
62
|
+
// 1. Normalize initial URL: BrowserRouter will see "/" not "/api/app-proxy/apps/X"
|
|
63
|
+
var _loc = location.pathname;
|
|
64
|
+
if (_loc.startsWith(BASE)) {
|
|
65
|
+
history.replaceState(history.state, '', _loc.slice(BASE.length) || '/');
|
|
66
|
+
}
|
|
67
|
+
// 2. Rewrite fetch() absolute paths through proxy (works on non-localhost HTTP)
|
|
68
|
+
var _fetch = window.fetch;
|
|
69
|
+
window.fetch = function(input, init) {
|
|
70
|
+
if (typeof input === 'string' && input.startsWith('/') && !input.startsWith('/api/app-proxy/')) {
|
|
71
|
+
input = BASE + input;
|
|
72
|
+
}
|
|
73
|
+
return _fetch.call(window, input, init);
|
|
74
|
+
};
|
|
75
|
+
// 3. Rewrite XMLHttpRequest absolute paths
|
|
76
|
+
var _xhrOpen = XMLHttpRequest.prototype.open;
|
|
77
|
+
XMLHttpRequest.prototype.open = function(method, url) {
|
|
78
|
+
if (typeof url === 'string' && url.startsWith('/') && !url.startsWith('/api/app-proxy/')) {
|
|
79
|
+
url = BASE + url;
|
|
80
|
+
}
|
|
81
|
+
return _xhrOpen.apply(this, [method, url].concat(Array.prototype.slice.call(arguments, 2)));
|
|
82
|
+
};
|
|
83
|
+
// 4. SW — best-effort for localhost/HTTPS; reloads once on first activation
|
|
84
|
+
if ('serviceWorker' in navigator) {
|
|
85
|
+
if (!navigator.serviceWorker.controller) {
|
|
86
|
+
navigator.serviceWorker.addEventListener('controllerchange', function() {
|
|
87
|
+
window.location.reload();
|
|
88
|
+
}, { once: true });
|
|
89
|
+
}
|
|
90
|
+
navigator.serviceWorker.register(BASE + '/sw-proxy.js?v=2', { scope: BASE + '/' })
|
|
91
|
+
.catch(function(){});
|
|
92
|
+
}
|
|
93
|
+
})();
|
|
94
|
+
</script>`;
|
|
95
|
+
out = out.replace(/<\/head>/i, `${patches}\n</head>`);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function rewriteCss(css: string, proxyBase: string): string {
|
|
100
|
+
return css.replace(/url\((['"]?)\/(?!\/)/g, `url($1${proxyBase}/`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── header helpers ────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function upstreamHeaders(
|
|
106
|
+
src: Headers,
|
|
107
|
+
port: number,
|
|
108
|
+
reqUrl: URL,
|
|
109
|
+
forceIdentity = false,
|
|
110
|
+
): Record<string, string> {
|
|
111
|
+
const out: Record<string, string> = {};
|
|
112
|
+
for (const [k, v] of src.entries()) {
|
|
113
|
+
if (HOP_BY_HOP.has(k.toLowerCase())) continue;
|
|
114
|
+
out[k] = v;
|
|
115
|
+
}
|
|
116
|
+
out["host"] = `localhost:${port}`;
|
|
117
|
+
out["x-forwarded-host"] = reqUrl.host;
|
|
118
|
+
out["x-forwarded-proto"] = reqUrl.protocol.replace(":", "");
|
|
119
|
+
if (forceIdentity) out["accept-encoding"] = "identity";
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildResHeaders(raw: Dispatcher.ResponseData["headers"]): Headers {
|
|
124
|
+
const out = new Headers();
|
|
125
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
126
|
+
if (!v || HOP_BY_HOP.has(k.toLowerCase())) continue;
|
|
127
|
+
const vals = Array.isArray(v) ? v : [v];
|
|
128
|
+
for (const val of vals) out.append(k, val);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── handler ───────────────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async function handleProxy(
|
|
136
|
+
request: Request,
|
|
137
|
+
{ params }: { params: Promise<{ path: string[] }> },
|
|
138
|
+
): Promise<Response> {
|
|
139
|
+
const segments = (await params).path ?? [];
|
|
140
|
+
const reqUrl = new URL(request.url);
|
|
141
|
+
|
|
142
|
+
const resolved = resolveByPrefix(segments);
|
|
143
|
+
if (!resolved) {
|
|
144
|
+
return NextResponse.json(
|
|
145
|
+
{ error: "App not running — launch it first in wiki-viewer." },
|
|
146
|
+
{ status: 503 },
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const { port, relPath, rest } = resolved;
|
|
151
|
+
const proxyBase = `/api/app-proxy/${relPath}`;
|
|
152
|
+
|
|
153
|
+
if (rest === "/sw-proxy.js") {
|
|
154
|
+
return new Response(makeServiceWorker(proxyBase), {
|
|
155
|
+
status: 200,
|
|
156
|
+
headers: {
|
|
157
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
158
|
+
"service-worker-allowed": proxyBase + "/",
|
|
159
|
+
"cache-control": "no-store",
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const upstreamUrl = `http://localhost:${port}${rest}${reqUrl.search}`;
|
|
165
|
+
const method = request.method as Dispatcher.HttpMethod;
|
|
166
|
+
const isBodyless = ["GET", "HEAD"].includes(request.method);
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
// First pass with normal headers to discover content-type
|
|
170
|
+
const first = await undiciRequest(upstreamUrl, {
|
|
171
|
+
method,
|
|
172
|
+
headers: upstreamHeaders(request.headers, port, reqUrl),
|
|
173
|
+
body: isBodyless ? null : (request.body as unknown as Readable),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const contentType = String(first.headers["content-type"] ?? "");
|
|
177
|
+
const needsRewrite = contentType.includes("text/html") || contentType.includes("text/css");
|
|
178
|
+
|
|
179
|
+
if (needsRewrite) {
|
|
180
|
+
// Drain first response and re-fetch with identity encoding for plain text
|
|
181
|
+
first.body.resume();
|
|
182
|
+
const second = await undiciRequest(upstreamUrl, {
|
|
183
|
+
method: isBodyless ? method : "GET",
|
|
184
|
+
headers: upstreamHeaders(request.headers, port, reqUrl, true),
|
|
185
|
+
body: null,
|
|
186
|
+
});
|
|
187
|
+
const text = await second.body.text();
|
|
188
|
+
const resHeaders = buildResHeaders(second.headers);
|
|
189
|
+
// Body changed size after rewriting — drop these or client truncates
|
|
190
|
+
resHeaders.delete("content-encoding");
|
|
191
|
+
resHeaders.delete("content-length");
|
|
192
|
+
|
|
193
|
+
if (contentType.includes("text/html")) {
|
|
194
|
+
resHeaders.set("content-type", "text/html; charset=utf-8");
|
|
195
|
+
return new Response(rewriteHtml(text, proxyBase), { status: second.statusCode, headers: resHeaders });
|
|
196
|
+
}
|
|
197
|
+
resHeaders.set("content-type", contentType);
|
|
198
|
+
return new Response(rewriteCss(text, proxyBase), { status: second.statusCode, headers: resHeaders });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// SPA fallback: if upstream returns 404 for a path with no file extension
|
|
202
|
+
// (i.e. a client-side route), re-fetch "/" and return index.html so the
|
|
203
|
+
// SPA's router can handle it client-side.
|
|
204
|
+
const hasExt = /\.[a-z0-9]{1,8}$/i.test(rest.split("?")[0]);
|
|
205
|
+
if (first.statusCode === 404 && !hasExt) {
|
|
206
|
+
first.body.resume();
|
|
207
|
+
const fallback = await undiciRequest(`http://localhost:${port}/`, {
|
|
208
|
+
method: "GET",
|
|
209
|
+
headers: upstreamHeaders(request.headers, port, reqUrl, true),
|
|
210
|
+
body: null,
|
|
211
|
+
});
|
|
212
|
+
const fallbackText = await fallback.body.text();
|
|
213
|
+
const fallbackHeaders = buildResHeaders(fallback.headers);
|
|
214
|
+
fallbackHeaders.delete("content-encoding");
|
|
215
|
+
fallbackHeaders.delete("content-length");
|
|
216
|
+
fallbackHeaders.set("content-type", "text/html; charset=utf-8");
|
|
217
|
+
return new Response(rewriteHtml(fallbackText, proxyBase), {
|
|
218
|
+
status: 200,
|
|
219
|
+
headers: fallbackHeaders,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 304/204/205: null-body statuses — Response constructor rejects a stream body
|
|
224
|
+
if (new Set([101, 204, 205, 304]).has(first.statusCode)) {
|
|
225
|
+
first.body.resume();
|
|
226
|
+
return new Response(null, { status: first.statusCode, headers: buildResHeaders(first.headers) });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Stream everything else — compressed bytes + Content-Encoding flow through intact
|
|
230
|
+
return new Response(Readable.toWeb(first.body) as ReadableStream, {
|
|
231
|
+
status: first.statusCode,
|
|
232
|
+
headers: buildResHeaders(first.headers),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
} catch (e) {
|
|
236
|
+
return NextResponse.json({ error: `Upstream unreachable: ${e}` }, { status: 502 });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const GET = handleProxy;
|
|
241
|
+
export const POST = handleProxy;
|
|
242
|
+
export const PUT = handleProxy;
|
|
243
|
+
export const DELETE = handleProxy;
|
|
244
|
+
export const PATCH = handleProxy;
|
|
245
|
+
export const HEAD = handleProxy;
|
|
246
|
+
export const OPTIONS = handleProxy;
|
|
247
|
+
export const dynamic = "force-dynamic";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
import { getRootDir } from "@/lib/root-dir";
|
|
5
|
+
|
|
6
|
+
const MIME_MAP: Record<string, string> = {
|
|
7
|
+
jpg: "image/jpeg",
|
|
8
|
+
jpeg: "image/jpeg",
|
|
9
|
+
png: "image/png",
|
|
10
|
+
gif: "image/gif",
|
|
11
|
+
webp: "image/webp",
|
|
12
|
+
svg: "image/svg+xml",
|
|
13
|
+
avif: "image/avif",
|
|
14
|
+
ico: "image/x-icon",
|
|
15
|
+
bmp: "image/bmp",
|
|
16
|
+
pdf: "application/pdf",
|
|
17
|
+
txt: "text/plain; charset=utf-8",
|
|
18
|
+
md: "text/markdown; charset=utf-8",
|
|
19
|
+
mp4: "video/mp4",
|
|
20
|
+
webm: "video/webm",
|
|
21
|
+
mov: "video/quicktime",
|
|
22
|
+
m4v: "video/mp4",
|
|
23
|
+
mp3: "audio/mpeg",
|
|
24
|
+
wav: "audio/wav",
|
|
25
|
+
ogg: "audio/ogg",
|
|
26
|
+
m4a: "audio/mp4",
|
|
27
|
+
aac: "audio/aac",
|
|
28
|
+
ipynb: "application/json",
|
|
29
|
+
json: "application/json",
|
|
30
|
+
js: "text/javascript",
|
|
31
|
+
ts: "text/plain",
|
|
32
|
+
css: "text/css",
|
|
33
|
+
html: "text/html",
|
|
34
|
+
mmd: "text/plain",
|
|
35
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
36
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
37
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export async function GET(
|
|
41
|
+
_request: Request,
|
|
42
|
+
{ params }: { params: Promise<{ path: string[] }> },
|
|
43
|
+
) {
|
|
44
|
+
const segments = (await params).path;
|
|
45
|
+
const rel = segments.join("/");
|
|
46
|
+
|
|
47
|
+
// Path traversal guard
|
|
48
|
+
const resolved = path.resolve(getRootDir(), rel);
|
|
49
|
+
if (resolved !== getRootDir() && !resolved.startsWith(getRootDir() + path.sep)) {
|
|
50
|
+
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const info = await stat(resolved);
|
|
55
|
+
if (info.isDirectory())
|
|
56
|
+
return NextResponse.json({ error: "Not a file" }, { status: 400 });
|
|
57
|
+
|
|
58
|
+
const ext = path.extname(resolved).slice(1).toLowerCase();
|
|
59
|
+
const contentType = MIME_MAP[ext] ?? "application/octet-stream";
|
|
60
|
+
const buffer = await readFile(resolved);
|
|
61
|
+
return new Response(buffer, {
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": contentType,
|
|
64
|
+
"Cache-Control": "private, max-age=60",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { NextResponse } from "next/server";
|
|
5
|
+
|
|
6
|
+
interface BrowseEntry {
|
|
7
|
+
name: string;
|
|
8
|
+
path: string;
|
|
9
|
+
accessible: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function GET(request: Request) {
|
|
13
|
+
const { searchParams } = new URL(request.url);
|
|
14
|
+
const rawPath = searchParams.get("path");
|
|
15
|
+
|
|
16
|
+
// Default to home dir
|
|
17
|
+
const target = rawPath ? path.resolve(rawPath) : os.homedir();
|
|
18
|
+
|
|
19
|
+
// Verify it's a directory
|
|
20
|
+
try {
|
|
21
|
+
const info = await stat(target);
|
|
22
|
+
if (!info.isDirectory()) {
|
|
23
|
+
return NextResponse.json({ error: "Not a directory" }, { status: 400 });
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return NextResponse.json({ error: "Path not found" }, { status: 404 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// List entries — only directories
|
|
30
|
+
let entries: BrowseEntry[] = [];
|
|
31
|
+
try {
|
|
32
|
+
const names = await readdir(target);
|
|
33
|
+
const infos = await Promise.allSettled(
|
|
34
|
+
names.map(async (name) => {
|
|
35
|
+
const full = path.join(target, name);
|
|
36
|
+
const info = await stat(full);
|
|
37
|
+
return { name, full, isDir: info.isDirectory() };
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
entries = infos
|
|
41
|
+
.filter(
|
|
42
|
+
(r): r is PromiseFulfilledResult<{ name: string; full: string; isDir: boolean }> =>
|
|
43
|
+
r.status === "fulfilled" && r.value.isDir,
|
|
44
|
+
)
|
|
45
|
+
.map((r) => ({ name: r.value.name, path: r.value.full, accessible: true }))
|
|
46
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
47
|
+
} catch {
|
|
48
|
+
// Permission denied or similar — return empty list but still allow selection
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parent path (null at filesystem root)
|
|
52
|
+
const parent = target === path.parse(target).root ? null : path.dirname(target);
|
|
53
|
+
|
|
54
|
+
// Quick-access shortcuts
|
|
55
|
+
const home = os.homedir();
|
|
56
|
+
const shortcuts = [
|
|
57
|
+
{ label: "Home", path: home },
|
|
58
|
+
{ label: "Root", path: "/" },
|
|
59
|
+
];
|
|
60
|
+
// Add common paths that exist
|
|
61
|
+
for (const [label, p] of [
|
|
62
|
+
["Desktop", path.join(home, "Desktop")],
|
|
63
|
+
["Documents", path.join(home, "Documents")],
|
|
64
|
+
["Downloads", path.join(home, "Downloads")],
|
|
65
|
+
] as [string, string][]) {
|
|
66
|
+
try {
|
|
67
|
+
await stat(p);
|
|
68
|
+
shortcuts.push({ label, path: p });
|
|
69
|
+
} catch {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return NextResponse.json({
|
|
73
|
+
path: target,
|
|
74
|
+
parent,
|
|
75
|
+
entries,
|
|
76
|
+
shortcuts,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { writeConfig } from "@/lib/config";
|
|
3
|
+
import { clearRootDir } from "@/lib/root-dir";
|
|
4
|
+
|
|
5
|
+
export async function POST() {
|
|
6
|
+
clearRootDir();
|
|
7
|
+
await writeConfig({ lastOpenedPath: undefined });
|
|
8
|
+
return NextResponse.json({ ok: true });
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { readConfig } from "@/lib/config";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const config = await readConfig();
|
|
6
|
+
return NextResponse.json({
|
|
7
|
+
pinnedPaths: config.pinnedPaths ?? [],
|
|
8
|
+
lastOpenedPath: config.lastOpenedPath ?? null,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { readConfig, writeConfig } from "@/lib/config";
|
|
4
|
+
|
|
5
|
+
export async function POST(request: Request) {
|
|
6
|
+
const body: { path?: string; action?: "pin" | "unpin" } = await request.json();
|
|
7
|
+
const p = body.path?.trim();
|
|
8
|
+
const action = body.action ?? "pin";
|
|
9
|
+
if (!p) return NextResponse.json({ error: "Missing path" }, { status: 400 });
|
|
10
|
+
|
|
11
|
+
if (action === "pin") {
|
|
12
|
+
try {
|
|
13
|
+
const info = await stat(p);
|
|
14
|
+
if (!info.isDirectory())
|
|
15
|
+
return NextResponse.json({ error: "Not a directory" }, { status: 400 });
|
|
16
|
+
} catch {
|
|
17
|
+
return NextResponse.json({ error: "Path not found" }, { status: 404 });
|
|
18
|
+
}
|
|
19
|
+
const config = await readConfig();
|
|
20
|
+
const pins = config.pinnedPaths ?? [];
|
|
21
|
+
if (!pins.includes(p)) {
|
|
22
|
+
await writeConfig({ pinnedPaths: [...pins, p] });
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
const config = await readConfig();
|
|
26
|
+
const pins = (config.pinnedPaths ?? []).filter((x) => x !== p);
|
|
27
|
+
await writeConfig({ pinnedPaths: pins });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return NextResponse.json({ ok: true });
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
import { getRootDir } from "@/lib/root-dir";
|
|
5
|
+
|
|
6
|
+
export async function POST(request: Request) {
|
|
7
|
+
const body: { path?: string } = await request.json();
|
|
8
|
+
const rel = body.path;
|
|
9
|
+
if (!rel || typeof rel !== "string")
|
|
10
|
+
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
|
11
|
+
|
|
12
|
+
// Path traversal guard
|
|
13
|
+
const resolved = path.resolve(getRootDir(), rel);
|
|
14
|
+
if (resolved !== getRootDir() && !resolved.startsWith(getRootDir() + path.sep))
|
|
15
|
+
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
|
16
|
+
|
|
17
|
+
// Open in system file manager
|
|
18
|
+
const platform = process.platform;
|
|
19
|
+
const cmd =
|
|
20
|
+
platform === "darwin"
|
|
21
|
+
? `open -R "${resolved}"`
|
|
22
|
+
: platform === "win32"
|
|
23
|
+
? `explorer /select,"${resolved}"`
|
|
24
|
+
: `xdg-open "${path.dirname(resolved)}"`;
|
|
25
|
+
|
|
26
|
+
exec(cmd, () => {});
|
|
27
|
+
return NextResponse.json({ ok: true });
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { writeConfig } from "@/lib/config";
|
|
4
|
+
import { setRootDir } from "@/lib/root-dir";
|
|
5
|
+
|
|
6
|
+
export async function POST(request: Request) {
|
|
7
|
+
const body: { path?: string } = await request.json();
|
|
8
|
+
const dir = body.path?.trim();
|
|
9
|
+
if (!dir) return NextResponse.json({ error: "Missing path" }, { status: 400 });
|
|
10
|
+
|
|
11
|
+
// Verify it exists and is a directory
|
|
12
|
+
try {
|
|
13
|
+
const info = await stat(dir);
|
|
14
|
+
if (!info.isDirectory())
|
|
15
|
+
return NextResponse.json({ error: "Not a directory" }, { status: 400 });
|
|
16
|
+
} catch {
|
|
17
|
+
return NextResponse.json({ error: "Directory not found" }, { status: 404 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
setRootDir(dir);
|
|
21
|
+
await writeConfig({ lastOpenedPath: dir });
|
|
22
|
+
return NextResponse.json({ ok: true, path: dir });
|
|
23
|
+
}
|