@inglorious/ssx 1.7.1 → 1.8.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/README.md +149 -6
- package/bin/ssx.js +32 -0
- package/package.json +5 -3
- package/src/dev/api.js +77 -0
- package/src/dev/index.js +8 -1
- package/src/serve/index.js +120 -0
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ SSX takes your entity-based web apps and generates optimized static HTML with fu
|
|
|
40
40
|
- **Optimized builds** - Minified, tree-shaken output
|
|
41
41
|
- **Source maps** - Debug production like development
|
|
42
42
|
- **Error boundaries** - Graceful failure handling
|
|
43
|
+
- **Serverless functions** - API routes with Web Fetch API
|
|
43
44
|
|
|
44
45
|
---
|
|
45
46
|
|
|
@@ -128,11 +129,17 @@ npm run build
|
|
|
128
129
|
# → Static site in dist/
|
|
129
130
|
```
|
|
130
131
|
|
|
131
|
-
###
|
|
132
|
+
### Preview
|
|
132
133
|
|
|
133
134
|
```bash
|
|
134
135
|
npm run preview
|
|
135
|
-
# →
|
|
136
|
+
# → Production server at http://localhost:3000 (serves static files + API routes)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Or use the CLI directly:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx ssx serve
|
|
136
143
|
```
|
|
137
144
|
|
|
138
145
|
Deploy `dist/` to:
|
|
@@ -486,6 +493,121 @@ title: My Post
|
|
|
486
493
|
This is a markdown page.
|
|
487
494
|
```
|
|
488
495
|
|
|
496
|
+
### ⚡ Serverless Functions (API Routes)
|
|
497
|
+
|
|
498
|
+
SSX supports serverless functions for dynamic API endpoints. Create files in `src/api/` that export HTTP method handlers:
|
|
499
|
+
|
|
500
|
+
```
|
|
501
|
+
src/api/
|
|
502
|
+
├── posts.js → /api/posts
|
|
503
|
+
├── posts/
|
|
504
|
+
│ └── [id].js → /api/posts/:id (alternative structure)
|
|
505
|
+
└── users.js → /api/users
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### Basic Example
|
|
509
|
+
|
|
510
|
+
```javascript
|
|
511
|
+
// src/api/posts.js
|
|
512
|
+
export async function GET(request) {
|
|
513
|
+
const posts = [
|
|
514
|
+
{ id: 1, title: "Hello World" },
|
|
515
|
+
{ id: 2, title: "My Second Post" },
|
|
516
|
+
]
|
|
517
|
+
|
|
518
|
+
return Response.json(posts)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export async function POST(request) {
|
|
522
|
+
const body = await request.json()
|
|
523
|
+
// Save to database...
|
|
524
|
+
return Response.json({ created: body }, { status: 201 })
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
#### Dynamic Routes
|
|
529
|
+
|
|
530
|
+
For routes like `/api/posts/:id`, export a single handler that parses the URL:
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
// src/api/posts.js
|
|
534
|
+
import { data } from "./posts-data.js"
|
|
535
|
+
|
|
536
|
+
export async function GET(request) {
|
|
537
|
+
const url = new URL(request.url)
|
|
538
|
+
const segments = url.pathname.split("/").filter(Boolean)
|
|
539
|
+
const id = segments[1] // /api/posts/:id
|
|
540
|
+
|
|
541
|
+
if (id) {
|
|
542
|
+
const post = data.find((post) => post.id === id)
|
|
543
|
+
if (!post) {
|
|
544
|
+
return Response.json({ error: "Not found" }, { status: 404 })
|
|
545
|
+
}
|
|
546
|
+
return Response.json(post)
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return Response.json(data)
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
#### Using with Pages
|
|
554
|
+
|
|
555
|
+
Fetch from your API in `routeChange` for client-side navigation, and use direct imports in `load` for build-time:
|
|
556
|
+
|
|
557
|
+
```javascript
|
|
558
|
+
// src/pages/posts/_slug.js
|
|
559
|
+
import { html } from "@inglorious/web"
|
|
560
|
+
|
|
561
|
+
export const post = {
|
|
562
|
+
async routeChange(entity, { route, params }, api) {
|
|
563
|
+
if (route !== entity.type) return
|
|
564
|
+
|
|
565
|
+
const entityId = entity.id
|
|
566
|
+
const response = await fetch(`/api/posts/${params.slug}`)
|
|
567
|
+
const post = await response.json()
|
|
568
|
+
post.body = renderMarkdown(post.body)
|
|
569
|
+
api.notify(`#${entityId}:dataFetchSuccess`, post)
|
|
570
|
+
},
|
|
571
|
+
|
|
572
|
+
dataFetchSuccess(entity, post) {
|
|
573
|
+
entity.post = post
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
render(entity) {
|
|
577
|
+
return html`<h1>${entity.post?.title}</h1>`
|
|
578
|
+
},
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Build-time: import directly (no HTTP overhead)
|
|
582
|
+
export async function load(entity, page) {
|
|
583
|
+
const { data } = await import("../../api/posts.js")
|
|
584
|
+
entity.post = data.find((p) => p.id === page.params.slug)
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
#### Request/Response API
|
|
589
|
+
|
|
590
|
+
Handlers receive a standard Web `Request` object and should return a `Response`:
|
|
591
|
+
|
|
592
|
+
```javascript
|
|
593
|
+
export async function GET(request) {
|
|
594
|
+
// Access request properties
|
|
595
|
+
const url = new URL(request.url)
|
|
596
|
+
const search = url.searchParams.get("q")
|
|
597
|
+
const headers = request.headers.get("authorization")
|
|
598
|
+
|
|
599
|
+
// Return JSON response
|
|
600
|
+
return Response.json({ results: [] })
|
|
601
|
+
|
|
602
|
+
// Or custom response
|
|
603
|
+
return new Response("Not found", { status: 404 })
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
#### Supported Methods
|
|
608
|
+
|
|
609
|
+
Export any of these HTTP methods: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`.
|
|
610
|
+
|
|
489
611
|
---
|
|
490
612
|
|
|
491
613
|
## CLI
|
|
@@ -520,12 +642,18 @@ Options:
|
|
|
520
642
|
-p, --port <port> Dev server port (default: 3000)
|
|
521
643
|
```
|
|
522
644
|
|
|
523
|
-
### `
|
|
645
|
+
### `ssx serve`
|
|
524
646
|
|
|
525
|
-
Serves the
|
|
647
|
+
Serves the production build with static files and API routes:
|
|
526
648
|
|
|
527
649
|
```bash
|
|
528
|
-
pnpm
|
|
650
|
+
pnpm ssx serve [options]
|
|
651
|
+
|
|
652
|
+
Options:
|
|
653
|
+
-c, --config <file> Config file (default: "site.config.js")
|
|
654
|
+
-r, --root <dir> Source root directory (default: ".")
|
|
655
|
+
-o, --out <dir> Output directory (default: "dist")
|
|
656
|
+
-p, --port <port> Server port (default: 3000)
|
|
529
657
|
```
|
|
530
658
|
|
|
531
659
|
---
|
|
@@ -541,6 +669,8 @@ my-site/
|
|
|
541
669
|
│ │ └── posts/
|
|
542
670
|
│ │ ├── index.js # /posts
|
|
543
671
|
│ │ └── _id.js # /posts/:id
|
|
672
|
+
│ ├── api/ # Serverless functions
|
|
673
|
+
│ │ └── posts.js # /api/posts
|
|
544
674
|
│ ├── store/ # Store configuration
|
|
545
675
|
│ │ └── entities.js # Entity definitions
|
|
546
676
|
│ └── types/ # Custom entity types (optional)
|
|
@@ -737,6 +867,19 @@ await dev({
|
|
|
737
867
|
})
|
|
738
868
|
```
|
|
739
869
|
|
|
870
|
+
### Production Server API
|
|
871
|
+
|
|
872
|
+
```javascript
|
|
873
|
+
import { serve } from "@inglorious/ssx/serve"
|
|
874
|
+
|
|
875
|
+
await serve({
|
|
876
|
+
rootDir: ".",
|
|
877
|
+
outDir: "dist",
|
|
878
|
+
port: 3000,
|
|
879
|
+
configFile: "site.config.js",
|
|
880
|
+
})
|
|
881
|
+
```
|
|
882
|
+
|
|
740
883
|
---
|
|
741
884
|
|
|
742
885
|
<!-- ## Examples
|
|
@@ -756,7 +899,7 @@ Check out these example projects:
|
|
|
756
899
|
- [x] Image optimization
|
|
757
900
|
- [x] Markdown support
|
|
758
901
|
- [x] i18n routing and locale-aware client navigation
|
|
759
|
-
- [
|
|
902
|
+
- [x] API routes (serverless functions)
|
|
760
903
|
|
|
761
904
|
---
|
|
762
905
|
|
package/bin/ssx.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Command } from "commander"
|
|
|
8
8
|
|
|
9
9
|
import { build } from "../src/build/index.js"
|
|
10
10
|
import { dev } from "../src/dev/index.js"
|
|
11
|
+
import { serve } from "../src/serve/index.js"
|
|
11
12
|
|
|
12
13
|
const __filename = fileURLToPath(import.meta.url)
|
|
13
14
|
const __dirname = path.dirname(__filename)
|
|
@@ -87,6 +88,37 @@ program
|
|
|
87
88
|
}
|
|
88
89
|
})
|
|
89
90
|
|
|
91
|
+
program
|
|
92
|
+
.command("serve")
|
|
93
|
+
.description("Serve production build with API routes")
|
|
94
|
+
.option("-c, --config <file>", "config file name", "site.config.js")
|
|
95
|
+
.option("-r, --root <dir>", "root directory", ".")
|
|
96
|
+
.option("-o, --out <dir>", "output directory", "dist")
|
|
97
|
+
.option("-p, --port <port>", "server port", 3000)
|
|
98
|
+
.action(async (options) => {
|
|
99
|
+
const cwd = process.cwd()
|
|
100
|
+
const rootDir = path.resolve(cwd, options.root)
|
|
101
|
+
const configPath = resolveConfigFile(rootDir, options.config)
|
|
102
|
+
const outDir = path.resolve(cwd, options.out)
|
|
103
|
+
const port = Number(options.port)
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await serve({
|
|
107
|
+
...options,
|
|
108
|
+
config: undefined,
|
|
109
|
+
root: undefined,
|
|
110
|
+
out: undefined,
|
|
111
|
+
configPath,
|
|
112
|
+
rootDir,
|
|
113
|
+
outDir,
|
|
114
|
+
port,
|
|
115
|
+
})
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error("Server failed:", error)
|
|
118
|
+
process.exit(1)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
90
122
|
program.parse()
|
|
91
123
|
|
|
92
124
|
function resolveConfigFile(rootDir, configFile) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ssx",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Server-Side-X. Xecution? Xperience? Who knows.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
29
29
|
".": "./types/index.d.ts",
|
|
30
|
+
"./dev": "./src/dev/index.js",
|
|
31
|
+
"./build": "./src/build/index.js",
|
|
32
|
+
"./serve": "./src/serve/index.js",
|
|
30
33
|
"./markdown": "./src/utils/markdown.js",
|
|
31
34
|
"./i18n": {
|
|
32
35
|
"types": "./types/i18n.d.ts",
|
|
@@ -65,7 +68,6 @@
|
|
|
65
68
|
},
|
|
66
69
|
"devDependencies": {
|
|
67
70
|
"prettier": "^3.6.2",
|
|
68
|
-
"serve": "^14.2.1",
|
|
69
71
|
"vitest": "^1.6.1",
|
|
70
72
|
"@inglorious/eslint-config": "1.1.2"
|
|
71
73
|
},
|
|
@@ -79,6 +81,6 @@
|
|
|
79
81
|
"test": "vitest run",
|
|
80
82
|
"dev": "node ./bin/ssx.js dev -r ./src/__fixtures__",
|
|
81
83
|
"build": "node ./bin/ssx.js build -r ./src/__fixtures__",
|
|
82
|
-
"preview": "serve
|
|
84
|
+
"preview": "node ./bin/ssx.js serve -r ./src/__fixtures__"
|
|
83
85
|
}
|
|
84
86
|
}
|
package/src/dev/api.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
export const createApiMiddleware = (rootDir, loader, onError) => {
|
|
5
|
+
return async (req, res, next) => {
|
|
6
|
+
const [urlPath] = req.url.split("?")
|
|
7
|
+
|
|
8
|
+
if (!urlPath.startsWith("/api/")) return next()
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const apiPath = urlPath.replace("/api/", "")
|
|
12
|
+
const segments = apiPath.split("/")
|
|
13
|
+
const searchPaths = []
|
|
14
|
+
|
|
15
|
+
for (let i = segments.length; i > 0; i--) {
|
|
16
|
+
searchPaths.push(segments.slice(0, i).join("/"))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let module = null
|
|
20
|
+
|
|
21
|
+
for (const searchPath of searchPaths) {
|
|
22
|
+
const apiFile = path.join(rootDir, "src", "api", `${searchPath}.js`)
|
|
23
|
+
if (existsSync(apiFile)) {
|
|
24
|
+
module = await loader(apiFile)
|
|
25
|
+
break
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!module) {
|
|
30
|
+
res.statusCode = 404
|
|
31
|
+
res.setHeader("Content-Type", "application/json")
|
|
32
|
+
res.end(JSON.stringify({ error: "Not found" }))
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const method = req.method.toUpperCase()
|
|
37
|
+
|
|
38
|
+
if (!module[method]) {
|
|
39
|
+
res.statusCode = 405
|
|
40
|
+
res.setHeader("Content-Type", "application/json")
|
|
41
|
+
res.end(JSON.stringify({ error: `Method ${method} not allowed` }))
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const request = createRequest(req)
|
|
46
|
+
const response = await module[method](request)
|
|
47
|
+
|
|
48
|
+
await sendResponse(res, response)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
onError?.(error)
|
|
51
|
+
next(error)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function createRequest(req) {
|
|
57
|
+
const protocol = req.socket.encrypted ? "https" : "http"
|
|
58
|
+
const host = req.headers.host || "localhost"
|
|
59
|
+
const url = `${protocol}://${host}${req.url}`
|
|
60
|
+
|
|
61
|
+
return new Request(url, {
|
|
62
|
+
method: req.method,
|
|
63
|
+
headers: req.headers,
|
|
64
|
+
body: req.method !== "GET" && req.method !== "HEAD" ? req : undefined,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function sendResponse(res, response) {
|
|
69
|
+
res.statusCode = response.status
|
|
70
|
+
|
|
71
|
+
for (const [key, value] of response.headers) {
|
|
72
|
+
res.setHeader(key, value)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const body = await response.text()
|
|
76
|
+
res.end(body)
|
|
77
|
+
}
|
package/src/dev/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import { getPages, matchRoute } from "../router/index.js"
|
|
|
8
8
|
import { generateApp } from "../scripts/app.js"
|
|
9
9
|
import { generateStore } from "../store/index.js"
|
|
10
10
|
import { loadConfig } from "../utils/config.js"
|
|
11
|
+
import { createApiMiddleware } from "./api.js"
|
|
11
12
|
import { createViteConfig, virtualFiles } from "./vite-config.js"
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -36,6 +37,11 @@ export async function dev(options = {}) {
|
|
|
36
37
|
const connectServer = connect()
|
|
37
38
|
connectServer.use(viteServer.middlewares)
|
|
38
39
|
|
|
40
|
+
const apiMiddleware = createApiMiddleware(rootDir, loader, (error) =>
|
|
41
|
+
viteServer.ssrFixStacktrace(error),
|
|
42
|
+
)
|
|
43
|
+
connectServer.use(apiMiddleware)
|
|
44
|
+
|
|
39
45
|
// Add SSR middleware
|
|
40
46
|
connectServer.use(async (req, res, next) => {
|
|
41
47
|
const [url] = req.url.split("?")
|
|
@@ -44,6 +50,7 @@ export async function dev(options = {}) {
|
|
|
44
50
|
// Skip special routes, static files, AND public assets
|
|
45
51
|
if (
|
|
46
52
|
url.startsWith("/@") ||
|
|
53
|
+
url.startsWith("/api/") ||
|
|
47
54
|
url.includes(".") || // Vite handles static files
|
|
48
55
|
url === "/favicon.ico"
|
|
49
56
|
) {
|
|
@@ -73,7 +80,7 @@ export async function dev(options = {}) {
|
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
// Generate and update the virtual app file BEFORE rendering
|
|
76
|
-
const app = generateApp(
|
|
83
|
+
const app = generateApp(pages, { ...mergedOptions, isDev: true })
|
|
77
84
|
virtualFiles.set("/main.js", app)
|
|
78
85
|
|
|
79
86
|
// Invalidate the virtual module to ensure Vite picks up changes
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { stat } from "node:fs/promises"
|
|
3
|
+
import http from "node:http"
|
|
4
|
+
import { createRequire } from "node:module"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { pathToFileURL } from "node:url"
|
|
7
|
+
|
|
8
|
+
import connect from "connect"
|
|
9
|
+
|
|
10
|
+
import { createApiMiddleware } from "../dev/api.js"
|
|
11
|
+
import { loadConfig } from "../utils/config.js"
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Starts a production server that serves static files and API routes.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} options - Configuration options.
|
|
19
|
+
* @returns {Promise<{close: Function}>} A promise that resolves to a server control object.
|
|
20
|
+
*/
|
|
21
|
+
export async function serve(options = {}) {
|
|
22
|
+
const config = await loadConfig(options)
|
|
23
|
+
|
|
24
|
+
const mergedOptions = { ...config, ...options }
|
|
25
|
+
const { rootDir = ".", outDir = "dist" } = mergedOptions
|
|
26
|
+
|
|
27
|
+
const { port = 3000 } = mergedOptions.vite?.server ?? {}
|
|
28
|
+
|
|
29
|
+
console.log("🚀 Starting production server...\n")
|
|
30
|
+
|
|
31
|
+
const connectServer = connect()
|
|
32
|
+
|
|
33
|
+
const distPath = path.resolve(process.cwd(), outDir)
|
|
34
|
+
const srcPath = path.resolve(process.cwd(), rootDir, "src")
|
|
35
|
+
|
|
36
|
+
const hasApi = existsSync(path.join(srcPath, "api"))
|
|
37
|
+
if (hasApi) {
|
|
38
|
+
const loader = (p) => import(pathToFileURL(p))
|
|
39
|
+
const apiMiddleware = createApiMiddleware(rootDir, loader)
|
|
40
|
+
connectServer.use(apiMiddleware)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
connectServer.use(serveStatic(distPath))
|
|
44
|
+
|
|
45
|
+
const server = http.createServer(connectServer)
|
|
46
|
+
|
|
47
|
+
await new Promise((resolve) => server.listen(port, resolve))
|
|
48
|
+
|
|
49
|
+
console.log(`\n✨ Production server running at http://localhost:${port}\n`)
|
|
50
|
+
console.log(`📁 Serving from: ${distPath}\n`)
|
|
51
|
+
if (hasApi) {
|
|
52
|
+
console.log(`⚡ API routes enabled from: ${path.join(srcPath, "api")}\n`)
|
|
53
|
+
}
|
|
54
|
+
console.log("Press Ctrl+C to stop\n")
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
close: () => server.close(),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function serveStatic(root) {
|
|
62
|
+
return async (req, res, next) => {
|
|
63
|
+
const [urlPath] = req.url.split("?")
|
|
64
|
+
|
|
65
|
+
let filePath = path.join(root, urlPath)
|
|
66
|
+
|
|
67
|
+
if (urlPath === "/" || urlPath === "") {
|
|
68
|
+
filePath = path.join(root, "index.html")
|
|
69
|
+
} else if (urlPath.endsWith("/")) {
|
|
70
|
+
filePath = path.join(root, urlPath, "index.html")
|
|
71
|
+
} else {
|
|
72
|
+
const stats = await stat(filePath).catch(() => null)
|
|
73
|
+
if (stats?.isDirectory()) {
|
|
74
|
+
filePath = path.join(filePath, "index.html")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!existsSync(filePath)) {
|
|
79
|
+
return next()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ext = path.extname(filePath)
|
|
83
|
+
const contentType = getContentType(ext)
|
|
84
|
+
|
|
85
|
+
res.setHeader("Content-Type", contentType)
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const content = require("fs").readFileSync(filePath)
|
|
89
|
+
res.end(content)
|
|
90
|
+
} catch {
|
|
91
|
+
next()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getContentType(ext) {
|
|
97
|
+
const types = {
|
|
98
|
+
".html": "text/html",
|
|
99
|
+
".css": "text/css",
|
|
100
|
+
".js": "application/javascript",
|
|
101
|
+
".mjs": "application/javascript",
|
|
102
|
+
".json": "application/json",
|
|
103
|
+
".png": "image/png",
|
|
104
|
+
".jpg": "image/jpeg",
|
|
105
|
+
".jpeg": "image/jpeg",
|
|
106
|
+
".gif": "image/gif",
|
|
107
|
+
".svg": "image/svg+xml",
|
|
108
|
+
".ico": "image/x-icon",
|
|
109
|
+
".webp": "image/webp",
|
|
110
|
+
".avif": "image/avif",
|
|
111
|
+
".woff": "font/woff",
|
|
112
|
+
".woff2": "font/woff2",
|
|
113
|
+
".ttf": "font/ttf",
|
|
114
|
+
".eot": "application/vnd.ms-fontobject",
|
|
115
|
+
".xml": "application/xml",
|
|
116
|
+
".txt": "text/plain",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return types[ext] || "application/octet-stream"
|
|
120
|
+
}
|