@toa.io/extensions.configuration 1.0.0-alpha.291 → 1.0.0-alpha.293

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.
Files changed (88) hide show
  1. package/package.json +16 -6
  2. package/transpiled/Aspect.d.ts +1 -1
  3. package/transpiled/Aspect.js +1 -1
  4. package/transpiled/Aspect.js.map +1 -1
  5. package/transpiled/Client.js +1 -1
  6. package/transpiled/Client.js.map +1 -1
  7. package/transpiled/Composition.js +1 -1
  8. package/transpiled/Composition.js.map +1 -1
  9. package/transpiled/Factory.d.ts +1 -1
  10. package/transpiled/UI.js +1 -1
  11. package/transpiled/UI.js.map +1 -1
  12. package/transpiled/configuration.d.ts +1 -1
  13. package/transpiled/configuration.js +1 -1
  14. package/transpiled/configuration.js.map +1 -1
  15. package/transpiled/index.d.ts +0 -3
  16. package/transpiled/index.js +0 -4
  17. package/transpiled/index.js.map +1 -1
  18. package/ui/dist/_app/immutable/chunks/BVp7FTPj.js +1 -0
  19. package/ui/dist/_app/immutable/chunks/{CNZAv2Es.js → Ba25-8aA.js} +1 -1
  20. package/ui/dist/_app/immutable/chunks/{BV5cDXGB.js → D5h0LnT-.js} +1 -1
  21. package/ui/dist/_app/immutable/chunks/{vl8APhvf.js → DJFe2eVw.js} +2 -2
  22. package/ui/dist/_app/immutable/chunks/{B_CH3nFk.js → iBEDxyKc.js} +1 -1
  23. package/ui/dist/_app/immutable/entry/{app.CK-db-gz.js → app.BaUwlvvj.js} +2 -2
  24. package/ui/dist/_app/immutable/entry/start.BQIMr3E0.js +1 -0
  25. package/ui/dist/_app/immutable/nodes/{0.vPwVABRs.js → 0.Bdnf_m1Q.js} +1 -1
  26. package/ui/dist/_app/immutable/nodes/{1.DS63PME5.js → 1.ChFvQfy2.js} +1 -1
  27. package/ui/dist/_app/immutable/nodes/{2.KRpWYBfz.js → 2.DoZuKZSw.js} +1 -1
  28. package/ui/dist/_app/immutable/nodes/3.BalX8tPE.js +1 -0
  29. package/ui/dist/_app/immutable/nodes/{4.D8lluH3u.js → 4.w2QdhCXS.js} +1 -1
  30. package/ui/dist/_app/version.json +1 -1
  31. package/ui/dist/index.html +10 -10
  32. package/CHANGELOG.md +0 -35
  33. package/components/configuration.values/source/lib/map.test.ts +0 -42
  34. package/components/configuration.values/tsconfig.tsbuildinfo +0 -1
  35. package/cucumber.mjs +0 -8
  36. package/features/site/_app/immutable/asset.js +0 -1
  37. package/features/site/favicon.ico +0 -0
  38. package/features/site/index.html +0 -10
  39. package/features/steps/UI.ts +0 -98
  40. package/features/steps/tsconfig.json +0 -9
  41. package/features/ui.feature +0 -93
  42. package/notes/consistency.md +0 -70
  43. package/schemas/annotation.cos.yaml +0 -29
  44. package/schemas/manifest.cos.yaml +0 -9
  45. package/source/Aspect.test.ts +0 -119
  46. package/source/Aspect.ts +0 -91
  47. package/source/Client.test.ts +0 -125
  48. package/source/Client.ts +0 -251
  49. package/source/Composition.ts +0 -57
  50. package/source/Factory.ts +0 -34
  51. package/source/Secret.test.ts +0 -32
  52. package/source/Secret.ts +0 -32
  53. package/source/UI.ts +0 -213
  54. package/source/configuration.test.ts +0 -152
  55. package/source/configuration.ts +0 -74
  56. package/source/const.ts +0 -30
  57. package/source/context.ts +0 -13
  58. package/source/deployment.test.ts +0 -112
  59. package/source/deployment.ts +0 -160
  60. package/source/epoch.test.ts +0 -35
  61. package/source/epoch.ts +0 -22
  62. package/source/index.ts +0 -10
  63. package/source/manifest.test.ts +0 -24
  64. package/source/manifest.ts +0 -13
  65. package/source/schemas.ts +0 -9
  66. package/transpiled/const.d.ts +0 -18
  67. package/transpiled/const.js +0 -22
  68. package/transpiled/const.js.map +0 -1
  69. package/transpiled/context.d.ts +0 -4
  70. package/transpiled/context.js +0 -9
  71. package/transpiled/context.js.map +0 -1
  72. package/transpiled/deployment.d.ts +0 -17
  73. package/transpiled/deployment.js +0 -107
  74. package/transpiled/deployment.js.map +0 -1
  75. package/transpiled/epoch.d.ts +0 -2
  76. package/transpiled/epoch.js +0 -19
  77. package/transpiled/epoch.js.map +0 -1
  78. package/transpiled/manifest.d.ts +0 -6
  79. package/transpiled/manifest.js +0 -6
  80. package/transpiled/manifest.js.map +0 -1
  81. package/transpiled/schemas.d.ts +0 -3
  82. package/transpiled/schemas.js +0 -7
  83. package/transpiled/schemas.js.map +0 -1
  84. package/tsconfig.json +0 -13
  85. package/tsconfig.tsbuildinfo +0 -1
  86. package/ui/dist/_app/immutable/chunks/3Kbn_1Vv.js +0 -1
  87. package/ui/dist/_app/immutable/entry/start.Bs7xr9Ne.js +0 -1
  88. package/ui/dist/_app/immutable/nodes/3.DCxORePS.js +0 -1
package/source/Client.ts DELETED
@@ -1,251 +0,0 @@
1
- import { console } from 'openspan'
2
- import { Connector, Locator } from '@toa.io/core'
3
- import { EVENT, SOURCE } from './const.js'
4
- import type { Remote } from '@toa.io/core'
5
- import type { Message } from '@toa.io/core/types'
6
- import type { Host } from './Factory.js'
7
-
8
- /**
9
- * One per process: one remote to the values service and one subscription to its events,
10
- * shared by every Aspect. What the Aspects ask for is collected and sent as one call;
11
- * what the service creates afterwards is handed to whoever subscribed.
12
- */
13
- export class Client extends Connector {
14
- /** Disconnected once, a connector keeps what it depended on, so a gone client is not reused. */
15
- public disposed = false
16
-
17
- private readonly host: Host
18
- private readonly options: Options
19
- private readonly pending = new Map<string, Pending>()
20
- private readonly listeners = new Map<string, Set<Listener>>()
21
- private remote: Remote | null = null
22
- private timer: NodeJS.Timeout | null = null
23
- private flushing = false
24
- private fresh = false
25
- private round = 0
26
-
27
- public constructor(host: Host, options: Partial<Options> = {}) {
28
- super()
29
-
30
- this.host = host
31
- this.options = { ...DEFAULTS, ...options }
32
- }
33
-
34
- /** The configuration of a component for an epoch, once the service has one. */
35
- public async fetch(component: string, epoch: string): Promise<Value> {
36
- const key = id(component, epoch)
37
-
38
- let entry = this.pending.get(key)
39
-
40
- if (entry === undefined) {
41
- entry = { component, epoch, waiters: [] }
42
- this.pending.set(key, entry)
43
- }
44
-
45
- const promise = new Promise<Value>((resolve) => {
46
- entry!.waiters.push(resolve)
47
- })
48
-
49
- if (this.flushing) this.fresh = true
50
- else this.schedule(0)
51
-
52
- return await promise
53
- }
54
-
55
- public subscribe(component: string, epoch: string, listener: Listener): void {
56
- const key = id(component, epoch)
57
-
58
- if (!this.listeners.has(key)) this.listeners.set(key, new Set())
59
-
60
- this.listeners.get(key)!.add(listener)
61
- }
62
-
63
- public unsubscribe(component: string, epoch: string, listener: Listener): void {
64
- this.listeners.get(id(component, epoch))?.delete(listener)
65
- }
66
-
67
- protected override async open(): Promise<void> {
68
- this.remote = await this.host.remote(LOCATOR, SOURCE)
69
-
70
- this.depends(this.remote)
71
- await this.remote.connect()
72
-
73
- const subscription = new Subscription(this.deliver.bind(this))
74
- const consumer = await this.host.receive(EVENT, subscription)
75
-
76
- this.depends(consumer)
77
- await consumer.connect()
78
- }
79
-
80
- protected override async close(): Promise<void> {
81
- if (this.timer !== null) {
82
- clearTimeout(this.timer)
83
- this.timer = null
84
- }
85
- }
86
-
87
- protected override async dispose(): Promise<void> {
88
- this.disposed = true
89
- }
90
-
91
- private schedule(delay: number): void {
92
- if (this.flushing) return
93
-
94
- if (this.timer !== null) {
95
- // a request that has just arrived does not wait for the round already planned
96
- if (delay > 0) return
97
-
98
- clearTimeout(this.timer)
99
- }
100
-
101
- this.timer = setTimeout(() => {
102
- void this.flush()
103
- }, delay)
104
- }
105
-
106
- private async flush(): Promise<void> {
107
- this.timer = null
108
- this.flushing = true
109
- this.fresh = false
110
-
111
- const batch = [...this.pending.values()]
112
- const input = batch.map(({ component, epoch }) => ({ component, epoch }))
113
-
114
- try {
115
- const output = await this.remote!.invoke<Fetched[] | Error>('fetch', { input })
116
-
117
- if (output instanceof Error) throw output
118
-
119
- this.settle(output)
120
- } catch (error) {
121
- console.warn('Configuration fetch failed', { error })
122
- } finally {
123
- this.flushing = false
124
- }
125
-
126
- this.report(batch)
127
-
128
- if (this.pending.size === 0) return
129
-
130
- if (this.fresh) this.schedule(0)
131
- else this.schedule(this.backoff())
132
- }
133
-
134
- /** Those the service has served are told; the rest stay for the next round. */
135
- private settle(output: Fetched[]): void {
136
- for (const { component, epoch, configuration, created } of output) {
137
- if (configuration === null) continue
138
-
139
- const key = id(component, epoch)
140
- const entry = this.pending.get(key)
141
-
142
- if (entry === undefined) continue
143
-
144
- this.pending.delete(key)
145
-
146
- for (const resolve of entry.waiters) resolve({ configuration, created })
147
- }
148
- }
149
-
150
- private report(batch: Pending[]): void {
151
- const waiting = batch
152
- .filter(({ component, epoch }) => this.pending.has(id(component, epoch)))
153
- .map(({ component }) => component)
154
-
155
- if (waiting.length === 0) {
156
- this.round = 0
157
-
158
- return
159
- }
160
-
161
- this.round++
162
-
163
- if (this.round % this.options.warn === 1)
164
- console.warn('Waiting for configuration', {
165
- components: waiting,
166
- round: this.round
167
- })
168
- }
169
-
170
- private backoff(): number {
171
- return Math.min(this.options.base * Math.pow(FACTOR, this.round), this.options.max)
172
- }
173
-
174
- /** A created object goes to the subscribers of its component and epoch, as it is. */
175
- private deliver(created: Created): void {
176
- const listeners = this.listeners.get(id(created.component, created.epoch))
177
-
178
- if (listeners === undefined) return
179
-
180
- const value: Value = {
181
- configuration: created.configuration,
182
- created: created.CREATED
183
- }
184
-
185
- for (const listener of listeners) listener(value)
186
- }
187
- }
188
-
189
- /** What the event consumer hands deliveries to. */
190
- class Subscription extends Connector {
191
- private readonly handler: (created: Created) => void
192
-
193
- public constructor(handler: (created: Created) => void) {
194
- super()
195
-
196
- this.handler = handler
197
- }
198
-
199
- public async receive(message: Message<Created>): Promise<void> {
200
- this.handler(message.payload)
201
- }
202
- }
203
-
204
- function id(component: string, epoch: string): string {
205
- return component + '\0' + epoch
206
- }
207
-
208
- const LOCATOR = new Locator('values', 'configuration')
209
- const FACTOR = 1.5
210
-
211
- const DEFAULTS: Options = {
212
- base: 1000,
213
- max: 10000,
214
- warn: 5
215
- }
216
-
217
- export interface Options {
218
- base: number
219
- max: number
220
- /** Every n-th round of waiting is reported. */
221
- warn: number
222
- }
223
-
224
- /** A configuration and when it was created; `0` for the deployed defaults. */
225
- export interface Value {
226
- configuration: object
227
- created: number
228
- }
229
-
230
- export interface Fetched {
231
- component: string
232
- epoch: string
233
- configuration: object | null
234
- created: number
235
- }
236
-
237
- /** The `configuration.values.created` payload: the object as stored. */
238
- export interface Created {
239
- component: string
240
- epoch: string
241
- configuration: object
242
- CREATED: number
243
- }
244
-
245
- export type Listener = (value: Value) => void
246
-
247
- interface Pending {
248
- component: string
249
- epoch: string
250
- waiters: Array<(value: Value) => void>
251
- }
@@ -1,57 +0,0 @@
1
- import { readdirSync, type Dirent } from 'node:fs'
2
- import { resolve } from 'node:path'
3
- import { Connector } from '@toa.io/core'
4
- import { uiPort } from './const.js'
5
- import { UI } from './UI.js'
6
- import type { Host } from './Factory.js'
7
-
8
- /** Hosts the values component in the service process. */
9
- export class Composition extends Connector {
10
- private readonly host: Host
11
-
12
- public constructor(host: Host) {
13
- super()
14
- this.host = host
15
- }
16
-
17
- protected override async open(): Promise<void> {
18
- const composition = await this.host.composition(components().paths)
19
-
20
- await composition.connect()
21
-
22
- this.depends(composition)
23
-
24
- // connected here rather than declared as a dependency: dependencies are connected
25
- // before `open` runs, so one added from inside it would never be
26
- const ui = new UI(uiPort())
27
-
28
- await ui.connect()
29
-
30
- this.depends(ui)
31
- }
32
- }
33
-
34
- export function components(): Components {
35
- const labels: string[] = []
36
- const paths: string[] = []
37
-
38
- for (const entry of entries()) {
39
- labels.push(entry.name.replace('.', '-'))
40
- paths.push(resolve(ROOT, entry.name))
41
- }
42
-
43
- return { labels, paths }
44
- }
45
-
46
- function entries(): Dirent[] {
47
- const entries = readdirSync(ROOT, { withFileTypes: true })
48
-
49
- return entries.filter((entry) => entry.isDirectory())
50
- }
51
-
52
- interface Components {
53
- labels: string[]
54
- paths: string[]
55
- }
56
-
57
- const ROOT = resolve(import.meta.dirname, '../components/')
package/source/Factory.ts DELETED
@@ -1,34 +0,0 @@
1
- import { type Connector, type Locator } from '@toa.io/core'
2
- import type { extensions } from '@toa.io/core/types'
3
- import { Aspect } from './Aspect.js'
4
- import { Client } from './Client.js'
5
- import { Composition } from './Composition.js'
6
- import { overridden } from './configuration.js'
7
- import type { Manifest } from './manifest.js'
8
-
9
- export class Factory implements extensions.Factory {
10
- private readonly host: Host
11
- private client: Client | null = null
12
-
13
- public constructor(host: Host) {
14
- this.host = host
15
- }
16
-
17
- public aspect(locator: Locator, manifest: Manifest): extensions.Aspect {
18
- const client = overridden(locator) ? null : this.shared()
19
-
20
- return new Aspect(locator, manifest, client)
21
- }
22
-
23
- public service(): Connector {
24
- return new Composition(this.host)
25
- }
26
-
27
- private shared(): Client {
28
- if (this.client === null || this.client.disposed) this.client = new Client(this.host)
29
-
30
- return this.client
31
- }
32
- }
33
-
34
- export type Host = extensions.Host
@@ -1,32 +0,0 @@
1
- import { it } from 'node:test'
2
- import assert from 'node:assert/strict'
3
-
4
- import { inspect } from 'node:util'
5
- import { REDACTED, Secret } from './Secret.js'
6
-
7
- const secret = new Secret('s3cret')
8
-
9
- it('should unwrap', () => {
10
- assert.deepStrictEqual(secret.unwrap(), 's3cret')
11
- })
12
-
13
- it('should not show as a string', () => {
14
- assert.deepStrictEqual(String(secret), REDACTED)
15
- assert.deepStrictEqual(`${secret}`, REDACTED)
16
- assert.deepStrictEqual('' + secret, REDACTED)
17
- })
18
-
19
- it('should not show in JSON', () => {
20
- assert.deepStrictEqual(JSON.stringify({ key: secret }), '{"key":"<REDACTED>"}')
21
- })
22
-
23
- it('should not show when inspected', () => {
24
- assert.deepStrictEqual(inspect(secret), REDACTED)
25
- assert.ok(inspect({ key: secret }).includes(REDACTED))
26
- assert.ok(!inspect({ key: secret }).includes('s3cret'))
27
- })
28
-
29
- it('should not expose the value as a property', () => {
30
- assert.deepStrictEqual(Object.keys(secret), [])
31
- assert.deepStrictEqual({ ...secret }, {})
32
- })
package/source/Secret.ts DELETED
@@ -1,32 +0,0 @@
1
- import { inspect } from 'node:util'
2
-
3
- /**
4
- * A configuration value that must not leak: a string only to whoever asks for it.
5
- *
6
- * This is what a value declared `format: secret` is on a component's context.
7
- */
8
- export class Secret {
9
- readonly #value: string
10
-
11
- public constructor(value: string) {
12
- this.#value = value
13
- }
14
-
15
- public unwrap(): string {
16
- return this.#value
17
- }
18
-
19
- public toString(): string {
20
- return REDACTED
21
- }
22
-
23
- public toJSON(): string {
24
- return REDACTED
25
- }
26
-
27
- public [inspect.custom](): string {
28
- return REDACTED
29
- }
30
- }
31
-
32
- export const REDACTED = '<REDACTED>'
package/source/UI.ts DELETED
@@ -1,213 +0,0 @@
1
- import * as fs from 'node:fs'
2
- import * as http from 'node:http'
3
- import * as path from 'node:path'
4
- import { Connector } from '@toa.io/core'
5
- import { console } from 'openspan'
6
- import { UI_PATH } from './const.js'
7
-
8
- /**
9
- * Serves the configuration UI: the directory `ui` builds, and nothing else.
10
- *
11
- * The page is a single-page application whose mount path is baked into the bundle,
12
- * so this server routes relative to `UI_PATH` — which is also what the ingress
13
- * forwards. `/configuration/*` is left alone: that is the component's own API.
14
- */
15
- export class UI extends Connector {
16
- private readonly server: http.Server = http.createServer()
17
- private readonly port: number
18
- private readonly root: string
19
-
20
- public constructor(port: number, root: string = SITE) {
21
- super()
22
-
23
- this.port = port
24
- this.root = root
25
- this.server.on('request', (request, response) => {
26
- this.listen(request, response)
27
- })
28
- }
29
-
30
- protected override async open(): Promise<void> {
31
- /*
32
- * A taken port is a real error: uniqueness across services is settled at export
33
- * time, so nothing here has to negotiate for one.
34
- */
35
- await new Promise<void>((resolve, reject) => {
36
- const failed = (error: Error): void => {
37
- this.server.off('listening', listening)
38
- reject(error)
39
- }
40
-
41
- const listening = (): void => {
42
- this.server.off('error', failed)
43
- resolve()
44
- }
45
-
46
- this.server.once('error', failed)
47
- this.server.once('listening', listening)
48
- this.server.listen(this.port)
49
- })
50
-
51
- if (!isFile(path.join(this.root, 'index.html')))
52
- console.warn('Configuration UI is not built', { root: this.root })
53
-
54
- console.info('Configuration UI started', { port: this.port, path: UI_PATH + '/' })
55
- }
56
-
57
- protected override async close(): Promise<void> {
58
- const closed = new Promise<void>((resolve) =>
59
- this.server.once('close', () => {
60
- resolve()
61
- })
62
- )
63
-
64
- this.server.close()
65
- this.server.closeAllConnections()
66
-
67
- await closed
68
- }
69
-
70
- private listen(request: http.IncomingMessage, response: http.ServerResponse): void {
71
- void this.respond(request, response).catch((error: Error) => {
72
- console.error('Configuration UI failure', { message: error.message })
73
-
74
- if (!response.writableEnded) response.writeHead(500).end()
75
- })
76
- }
77
-
78
- private async respond(
79
- request: http.IncomingMessage,
80
- response: http.ServerResponse
81
- ): Promise<void> {
82
- if (request.method !== 'GET' && request.method !== 'HEAD') {
83
- response.writeHead(405, { allow: 'GET, HEAD' }).end()
84
-
85
- return
86
- }
87
-
88
- const pathname = decode(request.url ?? '/')
89
-
90
- if (pathname === null) {
91
- response.writeHead(400).end()
92
-
93
- return
94
- }
95
-
96
- // the port is the explorer's alone, so the root is the way in
97
- if (pathname === '/') {
98
- response.writeHead(302, { location: UI_PATH + '/' }).end()
99
-
100
- return
101
- }
102
-
103
- const file = this.resolve(pathname)
104
-
105
- if (file === null) response.writeHead(404).end()
106
- else await this.send(file, request, response)
107
- }
108
-
109
- /**
110
- * The file a request lands on, or `null` when nothing does. A path that exists is
111
- * served as it is; anything else that could be a route falls back to the page,
112
- * because the client router — not this server — knows what routes there are.
113
- */
114
- private resolve(pathname: string): string | null {
115
- if (!pathname.startsWith(UI_PATH)) return null
116
-
117
- const relative = pathname.slice(UI_PATH.length)
118
-
119
- if (relative !== '' && !relative.startsWith('/')) return null
120
-
121
- const file = path.join(this.root, relative)
122
-
123
- if (file !== this.root && !file.startsWith(this.root + path.sep)) return null
124
-
125
- if (isFile(file)) return file
126
-
127
- /*
128
- * A missing asset is missing, but a route can look like one: `identity.passkeys` is a
129
- * component, not a file with an extension. What this server would have served is what
130
- * it knows how to serve, so anything else is a route and falls back to the page — as
131
- * does anything ending in a slash, which is no name for a file at all.
132
- */
133
- const asset = !relative.endsWith('/') && path.extname(relative) in TYPES
134
-
135
- return asset ? null : path.join(this.root, 'index.html')
136
- }
137
-
138
- private async send(
139
- file: string,
140
- request: http.IncomingMessage,
141
- response: http.ServerResponse
142
- ): Promise<void> {
143
- const stats = await fs.promises.stat(file).catch(() => null)
144
-
145
- if (stats === null) {
146
- response
147
- .writeHead(503, { 'content-type': 'text/plain' })
148
- .end('The configuration UI is not built. Run `npm run build:ui`.\n')
149
-
150
- return
151
- }
152
-
153
- response.writeHead(200, {
154
- 'content-type': TYPES[path.extname(file)] ?? 'application/octet-stream',
155
- 'content-length': stats.size,
156
- 'cache-control': caching(path.relative(this.root, file))
157
- })
158
-
159
- if (request.method === 'HEAD') response.end()
160
- else fs.createReadStream(file).pipe(response)
161
- }
162
- }
163
-
164
- function decode(url: string): string | null {
165
- try {
166
- return decodeURIComponent(url.split('?')[0])
167
- } catch {
168
- return null
169
- }
170
- }
171
-
172
- function isFile(file: string): boolean {
173
- return fs.existsSync(file) && fs.statSync(file).isFile()
174
- }
175
-
176
- /** Where `npm run build:ui` puts the page, from both `source` and `transpiled`. */
177
- const SITE = path.resolve(import.meta.dirname, '..', 'ui', 'dist')
178
-
179
- /** Assets under this prefix carry their build hash in the name. */
180
- const IMMUTABLE = path.join('_app', 'immutable')
181
- const FOREVER = 'public, max-age=31536000, immutable'
182
-
183
- /** The icons: named without a hash, so they are re-read, but rarely. */
184
- const ICONS = new Set(['favicon.ico', 'favicon-96x96.png', 'apple-touch-icon.png'])
185
- const DAY = 'public, max-age=86400'
186
-
187
- /**
188
- * How long what is served may be held. The page is never cached — it names the assets,
189
- * and their names carry the build. An icon is named for what it is rather than for its
190
- * content, so it is asked about again, but not on every page load.
191
- */
192
- function caching(relative: string): string {
193
- if (relative.startsWith(IMMUTABLE)) return FOREVER
194
-
195
- return ICONS.has(relative) ? DAY : 'no-cache'
196
- }
197
-
198
- const TYPES: Record<string, string> = {
199
- '.css': 'text/css; charset=utf-8',
200
- '.html': 'text/html; charset=utf-8',
201
- '.ico': 'image/x-icon',
202
- '.jpg': 'image/jpeg',
203
- '.js': 'text/javascript; charset=utf-8',
204
- '.json': 'application/json; charset=utf-8',
205
- '.map': 'application/json; charset=utf-8',
206
- '.png': 'image/png',
207
- '.svg': 'image/svg+xml',
208
- '.txt': 'text/plain; charset=utf-8',
209
- '.webmanifest': 'application/manifest+json',
210
- '.webp': 'image/webp',
211
- '.woff': 'font/woff',
212
- '.woff2': 'font/woff2'
213
- }