acrylblends 0.6.0 → 0.6.2
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 +8 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/api/presets.md +0 -3
- package/docs/framework/cordis/acryl-in-a-cordis-app.md +3 -0
- package/docs/framework/getting-started/basic.md +4 -4
- package/docs/framework/guides/jsx.md +1 -1
- package/docs/framework/guides/middleware.md +1 -1
- package/docs/framework/helpers/cookie.md +1 -1
- package/docs/framework/helpers/dev.md +0 -3
- package/docs/framework/helpers/jwt.md +1 -1
- package/docs/framework/helpers/proxy.md +0 -3
- package/docs/framework/middleware/builtin/basic-auth.md +6 -6
- package/docs/framework/middleware/builtin/cache.md +0 -3
- package/docs/framework/middleware/builtin/combine.md +1 -1
- package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
- package/docs/framework/middleware/builtin/logger.md +1 -1
- package/docs/framework/middleware/builtin/mount.md +0 -3
- package/docs/framework/upstream.json +2 -2
- package/lib/accepts.d.ts +1 -1
- package/lib/acryl-base.d.ts +1 -1
- package/lib/adapter.d.ts +1 -1
- package/lib/{app-CmAYW1Al.d.ts → app-BdrVHBlJ.d.ts} +1 -1
- package/lib/basic-auth.d.ts +1 -1
- package/lib/bearer-auth.d.ts +1 -1
- package/lib/body-limit.d.ts +1 -1
- package/lib/cache.d.ts +43 -0
- package/lib/cache.js +169 -0
- package/lib/client.d.ts +2 -2
- package/lib/combine.d.ts +1 -1
- package/lib/compress.d.ts +1 -1
- package/lib/conninfo.d.ts +2 -2
- package/lib/{context-Bm7rI-oK.d.ts → context-B5Y1lLha.d.ts} +1 -1
- package/lib/context-storage.d.ts +1 -1
- package/lib/{cookie-ClMaKDQf.d.ts → cookie-D9_nQbsM.d.ts} +1 -1
- package/lib/cookie.d.ts +3 -3
- package/lib/cors.d.ts +1 -1
- package/lib/csrf.d.ts +1 -1
- package/lib/dev.d.ts +19 -0
- package/lib/dev.js +50 -0
- package/lib/etag.d.ts +1 -1
- package/lib/factory.d.ts +2 -2
- package/lib/{index-B-3F4wt2.d.ts → index-DtEoak1i.d.ts} +2 -2
- package/lib/{index-UTBmM3GX.d.ts → index-Kqou8Fjh.d.ts} +2 -2
- package/lib/{index-oOClm_Xk.d.ts → index-m12zcmph.d.ts} +3 -3
- package/lib/index.d.ts +4 -4
- package/lib/ip-restriction.d.ts +2 -2
- package/lib/jwk.d.ts +4 -4
- package/lib/{jwt-Dg493Tai.d.ts → jwt-BA65QhnJ.d.ts} +1 -1
- package/lib/jwt.d.ts +4 -4
- package/lib/language.d.ts +1 -1
- package/lib/linear-router-C3FPib6h.js +96 -0
- package/lib/logger.d.ts +1 -1
- package/lib/loopback-only.d.ts +1 -1
- package/lib/method-not-allowed.d.ts +2 -2
- package/lib/method-override.d.ts +2 -2
- package/lib/mount.d.ts +44 -0
- package/lib/mount.js +67 -0
- package/lib/node-server/conninfo.d.ts +1 -1
- package/lib/node-server/serve-static.d.ts +1 -1
- package/lib/node-server.d.ts +1 -1
- package/lib/pattern-router-HZxjDGEQ.js +41 -0
- package/lib/powered-by.d.ts +1 -1
- package/lib/pretty-json.d.ts +1 -1
- package/lib/proxy.d.ts +72 -0
- package/lib/proxy.js +121 -0
- package/lib/quick.d.ts +8 -0
- package/lib/quick.js +17 -0
- package/lib/request-id.d.ts +1 -1
- package/lib/request.d.ts +1 -1
- package/lib/route.d.ts +1 -1
- package/lib/router/linear-router.js +1 -95
- package/lib/router/pattern-router.js +1 -40
- package/lib/schema-validator.d.ts +1 -1
- package/lib/secure-headers.d.ts +1 -1
- package/lib/serve-static.d.ts +1 -1
- package/lib/streaming.d.ts +1 -1
- package/lib/testing.d.ts +4 -4
- package/lib/timeout.d.ts +1 -1
- package/lib/timing.d.ts +1 -1
- package/lib/tiny.d.ts +8 -0
- package/lib/tiny.js +15 -0
- package/lib/trailing-slash.d.ts +1 -1
- package/lib/{types-CHesD1KG.d.ts → types-Bd29gh2o.d.ts} +3 -3
- package/lib/{types-CwrQjlaM.d.ts → types-D9DEXMjE.d.ts} +3 -1
- package/lib/{types-CDO6I3B0.d.ts → types-ZM9gIiKh.d.ts} +1 -1
- package/lib/types.d.ts +1 -1
- package/lib/utils/body.d.ts +1 -1
- package/lib/utils/cookie.d.ts +1 -1
- package/lib/utils/crypto.d.ts +1 -1
- package/lib/utils/ipaddr.d.ts +1 -1
- package/lib/utils/jwt.d.ts +1 -1
- package/lib/utils/types.d.ts +2 -2
- package/lib/validator.d.ts +2 -2
- package/lib/ws.d.ts +1 -1
- package/package.json +25 -1
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-node-server/upstream.json +1 -1
- package/plugins/acryl-router/src/core/helper/dev/index.test.ts +181 -0
- package/plugins/acryl-router/src/core/helper/dev/index.ts +79 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.test.ts +356 -0
- package/plugins/acryl-router/src/core/helper/proxy/index.ts +203 -0
- package/plugins/acryl-router/src/core/middleware/cache/index.test.ts +1211 -0
- package/plugins/acryl-router/src/core/middleware/cache/index.ts +323 -0
- package/plugins/acryl-router/src/core/middleware/mount/index.test.ts +334 -0
- package/plugins/acryl-router/src/core/middleware/mount/index.ts +108 -0
- package/plugins/acryl-router/src/core/preset/quick.test.ts +42 -0
- package/plugins/acryl-router/src/core/preset/quick.ts +24 -0
- package/plugins/acryl-router/src/core/preset/tiny.test.ts +42 -0
- package/plugins/acryl-router/src/core/preset/tiny.ts +20 -0
- package/plugins/acryl-router/src/core/utils/types.ts +3 -0
- package/plugins/acryl-router/src/helpers/index.ts +2 -0
- package/plugins/acryl-router/src/middleware/index.ts +2 -0
- package/plugins/acryl-router/src/service.ts +22 -5
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/main.ts +92 -0
- package/plugins/desktop-web/plugin.json +17 -0
- package/plugins/desktop-web/shutdown.ts +177 -0
- package/plugins/desktop-web/upstream.json +12 -0
- package/plugins/desktop-web/user-data.ts +38 -0
- package/plugins/desktop-web/window.ts +29 -0
- package/plugins/tsconfig.json +2 -0
- package/templates/todo/upstream.json +1 -1
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* Mount Helper for Acryl.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Context, ExecutionContext } from '../../context.ts'
|
|
7
|
+
import { routePath } from '../../helper/route/index.ts'
|
|
8
|
+
import type { MiddlewareHandler } from '../../types.ts'
|
|
9
|
+
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
type ApplicationHandler = (request: Request, ...args: any) => Response | Promise<Response>
|
|
12
|
+
type MountOptionHandler = (c: Context) => unknown
|
|
13
|
+
type MountReplaceRequest = (originalRequest: Request) => Request
|
|
14
|
+
type MountOptions =
|
|
15
|
+
| MountOptionHandler
|
|
16
|
+
| {
|
|
17
|
+
optionHandler?: MountOptionHandler
|
|
18
|
+
replaceRequest?: MountReplaceRequest | false
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultReplaceRequest = (c: Context): Request => {
|
|
22
|
+
// e.g. `/another-app/*` (basePath is already merged) -> `/another-app`
|
|
23
|
+
const pathPrefix = routePath(c).replace(/\/\*$/, '')
|
|
24
|
+
const url = new URL(c.req.raw.url)
|
|
25
|
+
url.pathname = c.req.path.slice(pathPrefix.length) || '/'
|
|
26
|
+
return new Request(url, c.req.raw)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const defaultGetOptions = (c: Context): unknown[] => {
|
|
30
|
+
let executionContext: ExecutionContext | undefined = undefined
|
|
31
|
+
try {
|
|
32
|
+
executionContext = c.executionCtx
|
|
33
|
+
} catch {} // Do nothing
|
|
34
|
+
return [c.env, executionContext]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `mount()` allows you to mount applications built with other frameworks into your Acryl application.
|
|
39
|
+
*
|
|
40
|
+
*
|
|
41
|
+
* @param {Function} applicationHandler - other Request Handler
|
|
42
|
+
* @param {MountOptions} [options] - options of `mount()`
|
|
43
|
+
* @returns {MiddlewareHandler} handler to register with `app.all()`
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { Router as IttyRouter } from 'itty-router'
|
|
48
|
+
* import { Acryl } from 'acrylblends'
|
|
49
|
+
* import { mount } from 'acrylblends/mount'
|
|
50
|
+
* // Create itty-router application
|
|
51
|
+
* const ittyRouter = IttyRouter()
|
|
52
|
+
* // GET /itty-router/hello
|
|
53
|
+
* ittyRouter.get('/hello', () => new Response('Hello from itty-router'))
|
|
54
|
+
*
|
|
55
|
+
* const app = new Acryl()
|
|
56
|
+
* app.all('/itty-router/*', mount(ittyRouter.handle))
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const app = new Acryl()
|
|
62
|
+
* // Send the request to another application without modification.
|
|
63
|
+
* app.all('/app/*', mount(anotherApp, {
|
|
64
|
+
* replaceRequest: (req) => req,
|
|
65
|
+
* }))
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export const mount = (
|
|
69
|
+
applicationHandler: ApplicationHandler,
|
|
70
|
+
options?: MountOptions
|
|
71
|
+
): MiddlewareHandler => {
|
|
72
|
+
// handle options
|
|
73
|
+
let replaceRequest: MountReplaceRequest | undefined
|
|
74
|
+
let optionHandler: MountOptionHandler | undefined
|
|
75
|
+
if (options) {
|
|
76
|
+
if (typeof options === 'function') {
|
|
77
|
+
optionHandler = options
|
|
78
|
+
} else {
|
|
79
|
+
optionHandler = options.optionHandler
|
|
80
|
+
if (options.replaceRequest === false) {
|
|
81
|
+
replaceRequest = (request) => request
|
|
82
|
+
} else {
|
|
83
|
+
replaceRequest = options.replaceRequest
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// prepare handlers for request
|
|
89
|
+
const getOptions: (c: Context) => unknown[] = optionHandler
|
|
90
|
+
? (c) => {
|
|
91
|
+
const options = optionHandler!(c)
|
|
92
|
+
return Array.isArray(options) ? options : [options]
|
|
93
|
+
}
|
|
94
|
+
: defaultGetOptions
|
|
95
|
+
|
|
96
|
+
return async (c, next) => {
|
|
97
|
+
const request = replaceRequest ? replaceRequest(c.req.raw) : defaultReplaceRequest(c)
|
|
98
|
+
const res = await applicationHandler(request, ...getOptions(c))
|
|
99
|
+
|
|
100
|
+
if (res) {
|
|
101
|
+
return res
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await next()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type { ApplicationHandler, MountOptionHandler, MountReplaceRequest, MountOptions }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getRouterName } from '../helper/dev/index.ts'
|
|
2
|
+
import { Acryl } from './quick.ts'
|
|
3
|
+
|
|
4
|
+
describe('acryl-router/quick preset', () => {
|
|
5
|
+
it('Should have SmartRouter + LinearRouter', async () => {
|
|
6
|
+
const app = new Acryl()
|
|
7
|
+
expect(getRouterName(app)).toBe('SmartRouter + LinearRouter')
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('Generics for Bindings and Variables', () => {
|
|
12
|
+
interface CloudflareBindings {
|
|
13
|
+
MY_VARIABLE: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
it('Should not throw type errors', () => {
|
|
17
|
+
// @ts-expect-error Bindings should extend object
|
|
18
|
+
new Acryl<{
|
|
19
|
+
Bindings: number
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const appWithInterface = new Acryl<{
|
|
23
|
+
Bindings: CloudflareBindings
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
appWithInterface.get('/', (c) => {
|
|
27
|
+
expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf<string>()
|
|
28
|
+
return c.text('/')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const appWithType = new Acryl<{
|
|
32
|
+
Bindings: {
|
|
33
|
+
foo: string
|
|
34
|
+
}
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
appWithType.get('/', (c) => {
|
|
38
|
+
expectTypeOf(c.env.foo).toMatchTypeOf<string>()
|
|
39
|
+
return c.text('Hello Acryl!')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* The preset that uses `LinearRouter`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AcrylBase } from '../app-base.ts'
|
|
7
|
+
import type { AcrylOptions } from '../app-base.ts'
|
|
8
|
+
import { LinearRouter } from '../router/linear-router/index.ts'
|
|
9
|
+
import { SmartRouter } from '../router/smart-router/index.ts'
|
|
10
|
+
import { TrieRouter } from '../router/trie-router/index.ts'
|
|
11
|
+
import type { BlankEnv, BlankSchema, Env, Schema } from '../types.ts'
|
|
12
|
+
|
|
13
|
+
export class Acryl<
|
|
14
|
+
E extends Env = BlankEnv,
|
|
15
|
+
S extends Schema = BlankSchema,
|
|
16
|
+
BasePath extends string = '/',
|
|
17
|
+
> extends AcrylBase<E, S, BasePath> {
|
|
18
|
+
constructor(options: AcrylOptions<E> = {}) {
|
|
19
|
+
super(options)
|
|
20
|
+
this.router = new SmartRouter({
|
|
21
|
+
routers: [new LinearRouter(), new TrieRouter()],
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getRouterName } from '../helper/dev/index.ts'
|
|
2
|
+
import { Acryl } from './tiny.ts'
|
|
3
|
+
|
|
4
|
+
describe('acryl-router/tiny preset', () => {
|
|
5
|
+
it('Should have PatternRouter', async () => {
|
|
6
|
+
const app = new Acryl()
|
|
7
|
+
expect(getRouterName(app)).toBe('PatternRouter')
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('Generics for Bindings and Variables', () => {
|
|
12
|
+
interface CloudflareBindings {
|
|
13
|
+
MY_VARIABLE: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
it('Should not throw type errors', () => {
|
|
17
|
+
// @ts-expect-error Bindings should extend object
|
|
18
|
+
new Acryl<{
|
|
19
|
+
Bindings: number
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const appWithInterface = new Acryl<{
|
|
23
|
+
Bindings: CloudflareBindings
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
appWithInterface.get('/', (c) => {
|
|
27
|
+
expectTypeOf(c.env.MY_VARIABLE).toMatchTypeOf<string>()
|
|
28
|
+
return c.text('/')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const appWithType = new Acryl<{
|
|
32
|
+
Bindings: {
|
|
33
|
+
foo: string
|
|
34
|
+
}
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
appWithType.get('/', (c) => {
|
|
38
|
+
expectTypeOf(c.env.foo).toMatchTypeOf<string>()
|
|
39
|
+
return c.text('Hello Acryl!')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module
|
|
3
|
+
* The preset that uses `PatternRouter`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AcrylBase } from '../app-base.ts'
|
|
7
|
+
import type { AcrylOptions } from '../app-base.ts'
|
|
8
|
+
import { PatternRouter } from '../router/pattern-router/index.ts'
|
|
9
|
+
import type { BlankEnv, BlankSchema, Env, Schema } from '../types.ts'
|
|
10
|
+
|
|
11
|
+
export class Acryl<
|
|
12
|
+
E extends Env = BlankEnv,
|
|
13
|
+
S extends Schema = BlankSchema,
|
|
14
|
+
BasePath extends string = '/',
|
|
15
|
+
> extends AcrylBase<E, S, BasePath> {
|
|
16
|
+
constructor(options: AcrylOptions<E> = {}) {
|
|
17
|
+
super(options)
|
|
18
|
+
this.router = new PatternRouter()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -125,6 +125,9 @@ export type BufferSource = ArrayBufferView<ArrayBuffer> | ArrayBuffer
|
|
|
125
125
|
/** A Web Crypto key, named from the runtime's own `crypto` so the public types do not need the DOM library. */
|
|
126
126
|
export type CryptoKey = Awaited<ReturnType<typeof crypto.subtle.importKey>>
|
|
127
127
|
|
|
128
|
+
/** What the Web `Headers` constructor accepts, named from the runtime's own `Headers` so the public types do not need the DOM library. */
|
|
129
|
+
export type HeadersInit = NonNullable<ConstructorParameters<typeof Headers>[0]>
|
|
130
|
+
|
|
128
131
|
/** How a WebSocket hands binary data to `onmessage`; declared here so the public types do not need the DOM library. */
|
|
129
132
|
export type BinaryType = 'arraybuffer' | 'blob'
|
|
130
133
|
|
|
@@ -5,6 +5,8 @@ export { createFactory, createMiddleware, Factory } from '../core/helper/factory
|
|
|
5
5
|
export { deleteCookie, generateCookie, generateSignedCookie, getCookie, getSignedCookie, setCookie, setSignedCookie } from '../core/helper/cookie/index.ts'
|
|
6
6
|
export { getExtension, getMimeType, mimes, type BaseMime } from '../core/utils/mime.ts'
|
|
7
7
|
export { html, raw } from '../core/helper/html/index.ts'
|
|
8
|
+
export { proxy } from '../core/helper/proxy/index.ts'
|
|
9
|
+
export { getRouterName, inspectRoutes, showRoutes } from '../core/helper/dev/index.ts'
|
|
8
10
|
export { matchedRoutes, routePath, baseRoutePath, basePath } from '../core/helper/route/index.ts'
|
|
9
11
|
export { testClient } from '../core/helper/testing/index.ts'
|
|
10
12
|
export { stream, streamSSE, streamText, SSEStreamingApi, type SSEMessage } from '../core/helper/streaming/index.ts'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { basicAuth } from '../core/middleware/basic-auth/index.ts'
|
|
2
2
|
export { bearerAuth } from '../core/middleware/bearer-auth/index.ts'
|
|
3
3
|
export { bodyLimit } from '../core/middleware/body-limit/index.ts'
|
|
4
|
+
export { cache } from '../core/middleware/cache/index.ts'
|
|
4
5
|
export { every, except, some } from '../core/middleware/combine/index.ts'
|
|
5
6
|
export { compress } from '../core/middleware/compress/index.ts'
|
|
6
7
|
export { contextStorage, getContext, tryGetContext } from '../core/middleware/context-storage/index.ts'
|
|
@@ -14,6 +15,7 @@ export { detectFromCookie, detectFromHeader, detectFromPath, detectFromQuery, la
|
|
|
14
15
|
export { logger } from '../core/middleware/logger/index.ts'
|
|
15
16
|
export { methodNotAllowed } from '../core/middleware/method-not-allowed/index.ts'
|
|
16
17
|
export { methodOverride } from '../core/middleware/method-override/index.ts'
|
|
18
|
+
export { mount } from '../core/middleware/mount/index.ts'
|
|
17
19
|
export { poweredBy } from '../core/middleware/powered-by/index.ts'
|
|
18
20
|
export { prettyJSON } from '../core/middleware/pretty-json/index.ts'
|
|
19
21
|
export { requestId, type RequestIdVariables } from '../core/middleware/request-id/index.ts'
|
|
@@ -54,13 +54,30 @@ declare module '@deepseek-ai/cordis' {
|
|
|
54
54
|
/** Stable Cordis plugin name, equal to the package name and the Loader row id. */
|
|
55
55
|
export const name = 'acryl-router'
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
/**
|
|
58
|
+
* What {@link Router.route} takes: any app with routes, read structurally. An `Acryl` from this package qualifies, and so does one built from
|
|
59
|
+
* another copy of the framework (the published `acrylblends`), whose classes TypeScript would refuse nominally for their private members:
|
|
60
|
+
* mounting copies the app's routes (and keeps its own `onError`), which is all the router reads. Their handlers are then called with this
|
|
61
|
+
* router's `Context`, which has the same shape.
|
|
62
|
+
*/
|
|
63
|
+
export interface MountableApp {
|
|
64
|
+
readonly routes: readonly MountableRoute[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One route of a {@link MountableApp}, as the core lists it. A handler of any context type fits (`never` parameters accept every function). */
|
|
68
|
+
export interface MountableRoute {
|
|
69
|
+
readonly basePath: string
|
|
70
|
+
readonly path: string
|
|
71
|
+
readonly method: string
|
|
72
|
+
readonly handler: (c: never, next: never) => unknown
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The core's `app.route` with a structural app: at run time it reads only the routes and the app's own error handler. */
|
|
76
|
+
const copyRoutes = (app: MountableApp): Acryl => new Acryl().route('/', app as unknown as Acryl)
|
|
60
77
|
|
|
61
78
|
interface Mount {
|
|
62
79
|
readonly path: string
|
|
63
|
-
readonly app:
|
|
80
|
+
readonly app: Acryl
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
interface RouterState {
|
|
@@ -161,7 +178,7 @@ export class Router extends Service {
|
|
|
161
178
|
* mount's code a request is in. The core's `app.route` runs first so a sub-app's own `onError` still applies to its routes.
|
|
162
179
|
*/
|
|
163
180
|
private snapshot(path: string, app: MountableApp): Mount {
|
|
164
|
-
const copy =
|
|
181
|
+
const copy = copyRoutes(app)
|
|
165
182
|
const tagged = new Acryl()
|
|
166
183
|
const mount: Mount = { path, app: tagged }
|
|
167
184
|
for (const route of copy.routes) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"source": {
|
|
3
3
|
"repo": "acryldev/acryl",
|
|
4
4
|
"path": "plugins/acryl-router",
|
|
5
|
-
"commit": "
|
|
5
|
+
"commit": "0174cb01625bfd32d0162ad56179c582f353321e"
|
|
6
6
|
},
|
|
7
7
|
"vendored": [
|
|
8
8
|
"src",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"LICENSE"
|
|
11
11
|
],
|
|
12
12
|
"rewrittenSpecifiers": 4,
|
|
13
|
-
"digest": "sha256:
|
|
13
|
+
"digest": "sha256:e732a94e1cf98c4390780156359603dc5d3d33597baf21457082fb33c120cb90",
|
|
14
14
|
"testsNotRun": {
|
|
15
15
|
"tests/loader.spec.ts": "loads the built lib/ entry through the real Loader; the library ships source and has no build"
|
|
16
16
|
},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"apps/acryl-desktop/src/shell/desktop-user-data.ts": "user-data.ts",
|
|
6
6
|
"apps/acryl-desktop/src/shell/shutdown.ts": "shutdown.ts"
|
|
7
7
|
},
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "0174cb01625bfd32d0162ad56179c582f353321e"
|
|
9
9
|
},
|
|
10
10
|
"rewrittenSpecifiers": 0,
|
|
11
11
|
"digest": "sha256:7dfe960a8bbb476c35540d2202a7aaa62b33492c2ac3e34314e2ecfca45f32f2"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The desktop shell of a plain Acryl app: `npm run desktop` serves the app (`src/app.ts`, `export default app`) on a free loopback port from this Electron main process and
|
|
3
|
+
* opens one window on it. Nothing in the app imports electron; every Electron call is in this file and `window.ts`, so a different shell is a change here only.
|
|
4
|
+
*
|
|
5
|
+
* Order matters: the user-data folder is set before anything else (and before the single-instance lock), so one app never shares window state, logs or the lock with
|
|
6
|
+
* another app or with the real user's data.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { mkdirSync, readFileSync } from 'node:fs'
|
|
10
|
+
import type { AddressInfo } from 'node:net'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { serve } from 'acrylblends/node-server'
|
|
14
|
+
import { app, BrowserWindow, shell } from 'electron'
|
|
15
|
+
import acrylApp from '../../app.ts'
|
|
16
|
+
import { createDesktopShutdown, installShutdownRequests } from './shutdown.ts'
|
|
17
|
+
import { resolveDesktopUserDataOverride } from './user-data.ts'
|
|
18
|
+
import { windowOptions } from './window.ts'
|
|
19
|
+
|
|
20
|
+
const projectRoot = fileURLToPath(new URL('../../..', import.meta.url))
|
|
21
|
+
const manifest = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8')) as {
|
|
22
|
+
name?: string
|
|
23
|
+
acrylblends?: { desktopPath?: string }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Electron's user-data folder: an explicit DSH_DESKTOP_USER_DATA, else a folder inside the app's own folder. */
|
|
27
|
+
const userData =
|
|
28
|
+
resolveDesktopUserDataOverride(process.env) ?? join(projectRoot, '.acrylblends', 'electron')
|
|
29
|
+
mkdirSync(userData, { recursive: true, mode: 0o700 })
|
|
30
|
+
app.setPath('userData', userData)
|
|
31
|
+
app.setName(manifest.name ?? 'acryl-app')
|
|
32
|
+
|
|
33
|
+
if (!app.requestSingleInstanceLock()) {
|
|
34
|
+
// The same app is already open: its window takes focus (see `second-instance` below) and this process ends.
|
|
35
|
+
app.quit()
|
|
36
|
+
} else {
|
|
37
|
+
let server: ReturnType<typeof serve> | undefined
|
|
38
|
+
let window: BrowserWindow | undefined
|
|
39
|
+
|
|
40
|
+
const shutdown = createDesktopShutdown(
|
|
41
|
+
() =>
|
|
42
|
+
new Promise<void>((done) => {
|
|
43
|
+
if (server === undefined) return done()
|
|
44
|
+
server.close(() => {
|
|
45
|
+
done()
|
|
46
|
+
})
|
|
47
|
+
// Open streams and keep-alive connections would hold close() open: end them.
|
|
48
|
+
;(server as { closeAllConnections?: () => void }).closeAllConnections?.()
|
|
49
|
+
}),
|
|
50
|
+
(code) => {
|
|
51
|
+
app.exit(code)
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
installShutdownRequests(process, app, (code) => {
|
|
55
|
+
void shutdown.request(code)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
app.on('second-instance', () => {
|
|
59
|
+
if (window === undefined) return
|
|
60
|
+
if (window.isMinimized()) window.restore()
|
|
61
|
+
window.focus()
|
|
62
|
+
})
|
|
63
|
+
// The window is the app: closing it ends the process on every platform, after the server has stopped.
|
|
64
|
+
app.on('window-all-closed', () => {
|
|
65
|
+
void shutdown.request(0)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
void app.whenReady().then(async () => {
|
|
69
|
+
const port = await new Promise<number>((resolve) => {
|
|
70
|
+
server = serve(
|
|
71
|
+
{ fetch: acrylApp.fetch, port: 0, hostname: '127.0.0.1' },
|
|
72
|
+
(info: AddressInfo) => {
|
|
73
|
+
resolve(info.port)
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
const origin = `http://127.0.0.1:${port}`
|
|
78
|
+
window = new BrowserWindow(windowOptions({ title: manifest.name ?? 'acryl-app' }))
|
|
79
|
+
window.once('ready-to-show', () => window?.show())
|
|
80
|
+
// Navigation stays on the app's own server; any other address opens in the system browser.
|
|
81
|
+
window.webContents.setWindowOpenHandler(({ url }) => {
|
|
82
|
+
void shell.openExternal(url)
|
|
83
|
+
return { action: 'deny' }
|
|
84
|
+
})
|
|
85
|
+
window.webContents.on('will-navigate', (event, url) => {
|
|
86
|
+
if (new URL(url).origin === origin) return
|
|
87
|
+
event.preventDefault()
|
|
88
|
+
void shell.openExternal(url)
|
|
89
|
+
})
|
|
90
|
+
await window.loadURL(new URL(manifest.acrylblends?.desktopPath ?? '/', origin).href)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "desktop-web",
|
|
3
|
+
"description": "Run a plain Acryl app (`export default app`) as a desktop app: an Electron window on the app's own server. `npm run desktop`; the app's code never imports electron. `acrylblends add desktop` picks this one for an app without the Blend plugins.",
|
|
4
|
+
"requires": [],
|
|
5
|
+
"inject": [],
|
|
6
|
+
"provides": [],
|
|
7
|
+
"dependencies": {},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"electron": "44.0.0"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"desktop": "electron src/plugins/desktop-web/main.ts"
|
|
13
|
+
},
|
|
14
|
+
"gitignore": [".acrylblends/"],
|
|
15
|
+
"omit": ["**/*.test.*"],
|
|
16
|
+
"mount": "nothing to mount: run `npm install`, then `npm run desktop` (the window opens on the app's own server; every electron call is in src/plugins/desktop-web)"
|
|
17
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** Application-level quit sources that must remain active before any window mounts. */
|
|
2
|
+
|
|
3
|
+
/** Maximum grace allowed for the Cordis tree to dispose before native exit. */
|
|
4
|
+
export const DESKTOP_SHUTDOWN_TIMEOUT_MS = 5_000
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Exit code a dev-mode relaunch uses instead of `app.relaunch()`.
|
|
8
|
+
*
|
|
9
|
+
* `acryl-desktop/scripts/launch-dev.mjs` stages the Electron runtime into a
|
|
10
|
+
* fresh, per-run temp directory and deletes it as soon as this process
|
|
11
|
+
* exits. `app.relaunch()` would spawn a new process pointed at that same
|
|
12
|
+
* (about to be deleted) executable path, which crashes at dyld load time
|
|
13
|
+
* ("Library not loaded: Electron Framework") instead of restarting the app.
|
|
14
|
+
* `main.ts` exits with this code under the dev launcher instead of calling
|
|
15
|
+
* `app.relaunch()`; `launch-dev.mjs` recognizes it and stages a fresh bundle
|
|
16
|
+
* before spawning again. The literal `43` is mirrored in `launch-dev.mjs` -
|
|
17
|
+
* that plain script cannot import this compiled module.
|
|
18
|
+
*/
|
|
19
|
+
export const DESKTOP_DEV_RESTART_EXIT_CODE = 43
|
|
20
|
+
|
|
21
|
+
/** Bounded, escalating shutdown controller for the Electron application. */
|
|
22
|
+
export interface DesktopShutdown {
|
|
23
|
+
/** Start graceful disposal, or force exit when a shutdown already owns it. */
|
|
24
|
+
request(code: number): Promise<void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Native operations performed after the Host tree has disposed. */
|
|
28
|
+
export interface DesktopNativeExit {
|
|
29
|
+
/** Mark the window close path as a final process exit. */
|
|
30
|
+
prepareToQuit(): void
|
|
31
|
+
/**
|
|
32
|
+
* Schedule a fresh Electron process and terminate the current one - fully
|
|
33
|
+
* owns process exit; `finish()` never calls `exit()` afterward. A packaged
|
|
34
|
+
* install's implementation must call both `app.relaunch()` (schedule) and
|
|
35
|
+
* `app.exit()` (terminate this instance) itself - `app.relaunch()` alone
|
|
36
|
+
* does not end the current process. Calling `exit()` again afterward with
|
|
37
|
+
* a different code is not a graceful no-op: two `app.exit()` calls in
|
|
38
|
+
* quick succession race, and the second one's code wins - confirmed live
|
|
39
|
+
* (`app.exit(DESKTOP_DEV_RESTART_EXIT_CODE)` from the dev-mode branch,
|
|
40
|
+
* immediately followed by `finish()`'s own unconditional `app.exit(0)`,
|
|
41
|
+
* silently replaced the dev-restart signal with a plain exit, so
|
|
42
|
+
* `launch-dev.mjs`'s loop saw code 0, concluded "not a restart," and
|
|
43
|
+
* returned instead of staging a fresh bundle and respawning - the app
|
|
44
|
+
* closed with no automatic restart, every time).
|
|
45
|
+
*/
|
|
46
|
+
relaunch(): void
|
|
47
|
+
/** End the current Electron process without another quit event. */
|
|
48
|
+
exit(code: number): void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Final-exit state shared by ordinary quits and mode-change relaunches. */
|
|
52
|
+
export interface DesktopExitCoordinator {
|
|
53
|
+
/** Mark the next successful exit as a relaunch. */
|
|
54
|
+
requestRelaunch(): void
|
|
55
|
+
/** Complete one native exit after Cordis teardown. */
|
|
56
|
+
finish(code: number): void
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Coordinate the final Electron action without relaunching failed generations.
|
|
61
|
+
* @param native - native application and runtime exit operations.
|
|
62
|
+
* @param beforeExit - listener cleanup that must precede app.exit.
|
|
63
|
+
* @returns a final-exit controller consumed by the shutdown path.
|
|
64
|
+
*/
|
|
65
|
+
export function createDesktopExitCoordinator(
|
|
66
|
+
native: DesktopNativeExit,
|
|
67
|
+
beforeExit: () => void,
|
|
68
|
+
): DesktopExitCoordinator {
|
|
69
|
+
let relaunchRequested = false
|
|
70
|
+
return {
|
|
71
|
+
requestRelaunch() {
|
|
72
|
+
relaunchRequested = true
|
|
73
|
+
},
|
|
74
|
+
finish(code) {
|
|
75
|
+
beforeExit()
|
|
76
|
+
native.prepareToQuit()
|
|
77
|
+
// relaunch() fully owns process termination (see its doc comment) -
|
|
78
|
+
// calling exit() afterward would race it and can silently replace
|
|
79
|
+
// whichever exit code relaunch() actually needs with this one.
|
|
80
|
+
if (relaunchRequested && code === 0) {
|
|
81
|
+
native.relaunch()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
native.exit(code)
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Create one bounded shutdown around the Host Cordis disposer.
|
|
91
|
+
* @param dispose - whole Host tree teardown.
|
|
92
|
+
* @param exit - final native process exit.
|
|
93
|
+
* @param timeoutMs - grace before forced exit.
|
|
94
|
+
* @returns a controller whose second request escalates immediately.
|
|
95
|
+
*/
|
|
96
|
+
export function createDesktopShutdown(
|
|
97
|
+
dispose: () => Promise<void>,
|
|
98
|
+
exit: (code: number) => void,
|
|
99
|
+
timeoutMs = DESKTOP_SHUTDOWN_TIMEOUT_MS,
|
|
100
|
+
): DesktopShutdown {
|
|
101
|
+
let pending: Promise<void> | undefined
|
|
102
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
103
|
+
let exited = false
|
|
104
|
+
|
|
105
|
+
const exitOnce = (code: number): void => {
|
|
106
|
+
if (exited) return
|
|
107
|
+
exited = true
|
|
108
|
+
if (timeout !== undefined) clearTimeout(timeout)
|
|
109
|
+
exit(code)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
request(code) {
|
|
114
|
+
if (pending !== undefined) {
|
|
115
|
+
exitOnce(code)
|
|
116
|
+
return pending
|
|
117
|
+
}
|
|
118
|
+
const failureCode = code === 0 ? 1 : code
|
|
119
|
+
timeout = setTimeout(() => { exitOnce(failureCode) }, timeoutMs)
|
|
120
|
+
pending = Promise.resolve().then(dispose).then(
|
|
121
|
+
() => { exitOnce(code) },
|
|
122
|
+
() => { exitOnce(failureCode) },
|
|
123
|
+
)
|
|
124
|
+
return pending
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Minimal native quit event consumed by the launcher. */
|
|
130
|
+
export interface DesktopQuitEvent {
|
|
131
|
+
/** Cancel Electron's immediate default exit. */
|
|
132
|
+
preventDefault(): void
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Process signal surface used by the shutdown coordinator. */
|
|
136
|
+
export interface DesktopSignalSource {
|
|
137
|
+
/** Register one shutdown signal listener. */
|
|
138
|
+
on(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown
|
|
139
|
+
/** Remove one shutdown signal listener. */
|
|
140
|
+
off(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Electron application surface used by the shutdown coordinator. */
|
|
144
|
+
export interface DesktopQuitSource {
|
|
145
|
+
/** Register the native quit guard. */
|
|
146
|
+
on(event: 'before-quit', listener: (event: DesktopQuitEvent) => void): unknown
|
|
147
|
+
/** Remove the native quit guard. */
|
|
148
|
+
off(event: 'before-quit', listener: (event: DesktopQuitEvent) => void): unknown
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Route every application-level quit source through Cordis teardown.
|
|
153
|
+
* @param signals - process signal owner.
|
|
154
|
+
* @param nativeApp - Electron application event owner.
|
|
155
|
+
* @param requestQuit - idempotent launcher shutdown request.
|
|
156
|
+
* @returns a disposer removing every listener.
|
|
157
|
+
*/
|
|
158
|
+
export function installShutdownRequests(
|
|
159
|
+
signals: DesktopSignalSource,
|
|
160
|
+
nativeApp: DesktopQuitSource,
|
|
161
|
+
requestQuit: (code: number) => void,
|
|
162
|
+
): () => void {
|
|
163
|
+
const interrupt = (): void => { requestQuit(130) }
|
|
164
|
+
const terminate = (): void => { requestQuit(0) }
|
|
165
|
+
const beforeQuit = (event: DesktopQuitEvent): void => {
|
|
166
|
+
event.preventDefault()
|
|
167
|
+
requestQuit(0)
|
|
168
|
+
}
|
|
169
|
+
signals.on('SIGINT', interrupt)
|
|
170
|
+
signals.on('SIGTERM', terminate)
|
|
171
|
+
nativeApp.on('before-quit', beforeQuit)
|
|
172
|
+
return () => {
|
|
173
|
+
signals.off('SIGINT', interrupt)
|
|
174
|
+
signals.off('SIGTERM', terminate)
|
|
175
|
+
nativeApp.off('before-quit', beforeQuit)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"source": {
|
|
3
|
+
"repo": "acryldev/acryl",
|
|
4
|
+
"files": {
|
|
5
|
+
"apps/acryl-desktop/src/shell/desktop-user-data.ts": "user-data.ts",
|
|
6
|
+
"apps/acryl-desktop/src/shell/shutdown.ts": "shutdown.ts"
|
|
7
|
+
},
|
|
8
|
+
"commit": "0174cb01625bfd32d0162ad56179c582f353321e"
|
|
9
|
+
},
|
|
10
|
+
"rewrittenSpecifiers": 0,
|
|
11
|
+
"digest": "sha256:7dfe960a8bbb476c35540d2202a7aaa62b33492c2ac3e34314e2ecfca45f32f2"
|
|
12
|
+
}
|