skybridge 0.0.0-dev.ff50fdb → 0.0.0-dev.ffaef7a
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 +124 -114
- package/dist/cli/build-helpers.d.ts +10 -0
- package/dist/cli/build-helpers.js +93 -0
- package/dist/cli/build-helpers.js.map +1 -0
- package/dist/cli/build-helpers.test.js +89 -0
- package/dist/cli/build-helpers.test.js.map +1 -0
- package/dist/cli/build-steps.d.ts +2 -0
- package/dist/cli/build-steps.js +68 -0
- package/dist/cli/build-steps.js.map +1 -0
- package/dist/cli/build-steps.test.js +52 -0
- package/dist/cli/build-steps.test.js.map +1 -0
- package/dist/cli/detect-port.d.ts +18 -0
- package/dist/cli/detect-port.js +50 -0
- package/dist/cli/detect-port.js.map +1 -0
- package/dist/cli/header.d.ts +1 -1
- package/dist/cli/header.js +1 -1
- package/dist/cli/header.js.map +1 -1
- package/dist/cli/resolve-views-dir.d.ts +1 -0
- package/dist/cli/resolve-views-dir.js +17 -0
- package/dist/cli/resolve-views-dir.js.map +1 -0
- package/dist/cli/run-command.js.map +1 -1
- package/dist/cli/run-plain.d.ts +18 -0
- package/dist/cli/run-plain.js +89 -0
- package/dist/cli/run-plain.js.map +1 -0
- package/dist/cli/telemetry.js.map +1 -1
- package/dist/cli/tunnel-control-server.d.ts +9 -0
- package/dist/cli/tunnel-control-server.js +31 -0
- package/dist/cli/tunnel-control-server.js.map +1 -0
- package/dist/cli/tunnel-control-server.test.d.ts +1 -0
- package/dist/cli/tunnel-control-server.test.js +39 -0
- package/dist/cli/tunnel-control-server.test.js.map +1 -0
- package/dist/cli/tunnel-handler.d.ts +3 -0
- package/dist/cli/tunnel-handler.js +48 -0
- package/dist/cli/tunnel-handler.js.map +1 -0
- package/dist/cli/tunnel-handler.test.d.ts +1 -0
- package/dist/cli/tunnel-handler.test.js +105 -0
- package/dist/cli/tunnel-handler.test.js.map +1 -0
- package/dist/cli/tunnel.d.ts +57 -0
- package/dist/cli/tunnel.js +154 -0
- package/dist/cli/tunnel.js.map +1 -0
- package/dist/cli/tunnel.test.d.ts +1 -0
- package/dist/cli/tunnel.test.js +190 -0
- package/dist/cli/tunnel.test.js.map +1 -0
- package/dist/cli/types.d.ts +5 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/use-execute-steps.d.ts +3 -2
- package/dist/cli/use-execute-steps.js +6 -1
- package/dist/cli/use-execute-steps.js.map +1 -1
- package/dist/cli/use-messages.d.ts +3 -0
- package/dist/cli/use-messages.js +11 -0
- package/dist/cli/use-messages.js.map +1 -0
- package/dist/cli/use-nodemon.d.ts +16 -0
- package/dist/cli/use-nodemon.js +84 -0
- package/dist/cli/use-nodemon.js.map +1 -0
- package/dist/cli/use-open-browser.d.ts +1 -0
- package/dist/cli/use-open-browser.js +44 -0
- package/dist/cli/use-open-browser.js.map +1 -0
- package/dist/cli/use-tunnel.d.ts +14 -0
- package/dist/cli/use-tunnel.js +131 -0
- package/dist/cli/use-tunnel.js.map +1 -0
- package/dist/cli/use-typescript-check.d.ts +15 -0
- package/dist/cli/use-typescript-check.js +97 -0
- package/dist/cli/use-typescript-check.js.map +1 -0
- package/dist/commands/build.d.ts +0 -3
- package/dist/commands/build.js +3 -16
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/create.d.ts +9 -0
- package/dist/commands/create.js +30 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/dev.d.ts +5 -1
- package/dist/commands/dev.js +109 -13
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.d.ts +3 -1
- package/dist/commands/start.js +37 -15
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/telemetry/disable.js.map +1 -1
- package/dist/commands/telemetry/enable.js.map +1 -1
- package/dist/commands/telemetry/status.js.map +1 -1
- package/dist/server/asset-base-url-transform-plugin.d.ts +11 -0
- package/dist/server/asset-base-url-transform-plugin.js +48 -0
- package/dist/server/asset-base-url-transform-plugin.js.map +1 -0
- package/dist/server/asset-base-url-transform-plugin.test.d.ts +1 -0
- package/dist/server/asset-base-url-transform-plugin.test.js +134 -0
- package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -0
- package/dist/server/auth/discovery.d.ts +32 -0
- package/dist/server/auth/discovery.js +56 -0
- package/dist/server/auth/discovery.js.map +1 -0
- package/dist/server/auth/discovery.test.d.ts +1 -0
- package/dist/server/auth/discovery.test.js +93 -0
- package/dist/server/auth/discovery.test.js.map +1 -0
- package/dist/server/auth/index.d.ts +18 -0
- package/dist/server/auth/index.js +2 -0
- package/dist/server/auth/index.js.map +1 -0
- package/dist/server/auth/providers/auth0.d.ts +18 -0
- package/dist/server/auth/providers/auth0.js +31 -0
- package/dist/server/auth/providers/auth0.js.map +1 -0
- package/dist/server/auth/providers/auth0.test.d.ts +1 -0
- package/dist/server/auth/providers/auth0.test.js +48 -0
- package/dist/server/auth/providers/auth0.test.js.map +1 -0
- package/dist/server/auth/providers/clerk.d.ts +14 -0
- package/dist/server/auth/providers/clerk.js +16 -0
- package/dist/server/auth/providers/clerk.js.map +1 -0
- package/dist/server/auth/providers/clerk.test.d.ts +1 -0
- package/dist/server/auth/providers/clerk.test.js +28 -0
- package/dist/server/auth/providers/clerk.test.js.map +1 -0
- package/dist/server/auth/providers/custom.d.ts +24 -0
- package/dist/server/auth/providers/custom.js +37 -0
- package/dist/server/auth/providers/custom.js.map +1 -0
- package/dist/server/auth/providers/custom.test.d.ts +1 -0
- package/dist/server/auth/providers/custom.test.js +107 -0
- package/dist/server/auth/providers/custom.test.js.map +1 -0
- package/dist/server/auth/providers/descope.d.ts +15 -0
- package/dist/server/auth/providers/descope.js +33 -0
- package/dist/server/auth/providers/descope.js.map +1 -0
- package/dist/server/auth/providers/descope.test.d.ts +1 -0
- package/dist/server/auth/providers/descope.test.js +37 -0
- package/dist/server/auth/providers/descope.test.js.map +1 -0
- package/dist/server/auth/providers/shared.d.ts +2 -0
- package/dist/server/auth/providers/shared.js +6 -0
- package/dist/server/auth/providers/shared.js.map +1 -0
- package/dist/server/auth/providers/shared.test.d.ts +1 -0
- package/dist/server/auth/providers/shared.test.js +10 -0
- package/dist/server/auth/providers/shared.test.js.map +1 -0
- package/dist/server/auth/providers/stytch.d.ts +12 -0
- package/dist/server/auth/providers/stytch.js +13 -0
- package/dist/server/auth/providers/stytch.js.map +1 -0
- package/dist/server/auth/providers/workos.d.ts +11 -0
- package/dist/server/auth/providers/workos.js +12 -0
- package/dist/server/auth/providers/workos.js.map +1 -0
- package/dist/server/auth/security-schemes.d.ts +13 -0
- package/dist/server/auth/security-schemes.js +53 -0
- package/dist/server/auth/security-schemes.js.map +1 -0
- package/dist/server/auth/security-schemes.test.d.ts +1 -0
- package/dist/server/auth/security-schemes.test.js +90 -0
- package/dist/server/auth/security-schemes.test.js.map +1 -0
- package/dist/server/auth/setup.d.ts +6 -0
- package/dist/server/auth/setup.js +98 -0
- package/dist/server/auth/setup.js.map +1 -0
- package/dist/server/auth/setup.test.d.ts +1 -0
- package/dist/server/auth/setup.test.js +450 -0
- package/dist/server/auth/setup.test.js.map +1 -0
- package/dist/server/auth/verify.d.ts +12 -0
- package/dist/server/auth/verify.js +38 -0
- package/dist/server/auth/verify.js.map +1 -0
- package/dist/server/auth/verify.test.d.ts +1 -0
- package/dist/server/auth/verify.test.js +100 -0
- package/dist/server/auth/verify.test.js.map +1 -0
- package/dist/server/auth.d.ts +20 -0
- package/dist/server/auth.js +28 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/build-manifest.test.d.ts +1 -0
- package/dist/server/build-manifest.test.js +27 -0
- package/dist/server/build-manifest.test.js.map +1 -0
- package/dist/server/content-helpers.d.ts +67 -0
- package/dist/server/content-helpers.js +79 -0
- package/dist/server/content-helpers.js.map +1 -0
- package/dist/server/content-helpers.test.d.ts +1 -0
- package/dist/server/content-helpers.test.js +70 -0
- package/dist/server/content-helpers.test.js.map +1 -0
- package/dist/server/express.d.ts +11 -0
- package/dist/server/express.js +101 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/express.test.d.ts +1 -0
- package/dist/server/express.test.js +491 -0
- package/dist/server/express.test.js.map +1 -0
- package/dist/server/file-ref.d.ts +28 -0
- package/dist/server/file-ref.js +27 -0
- package/dist/server/file-ref.js.map +1 -0
- package/dist/server/index.d.ts +14 -3
- package/dist/server/index.js +11 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/inferUtilityTypes.d.ts +6 -6
- package/dist/server/inferUtilityTypes.js.map +1 -1
- package/dist/server/metric.d.ts +14 -0
- package/dist/server/metric.js +62 -0
- package/dist/server/metric.js.map +1 -0
- package/dist/server/middleware.d.ts +137 -0
- package/dist/server/middleware.js +93 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/middleware.test-d.d.ts +1 -0
- package/dist/server/middleware.test-d.js +75 -0
- package/dist/server/middleware.test-d.js.map +1 -0
- package/dist/server/middleware.test.d.ts +1 -0
- package/dist/server/middleware.test.js +493 -0
- package/dist/server/middleware.test.js.map +1 -0
- package/dist/server/requestOrigin.d.ts +7 -0
- package/dist/server/requestOrigin.js +25 -0
- package/dist/server/requestOrigin.js.map +1 -0
- package/dist/server/server.d.ts +391 -58
- package/dist/server/server.js +644 -97
- package/dist/server/server.js.map +1 -1
- package/dist/server/templateHelper.d.ts +5 -7
- package/dist/server/templateHelper.js +3 -22
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.d.ts +4 -0
- package/dist/server/templates.generated.js +47 -0
- package/dist/server/templates.generated.js.map +1 -0
- package/dist/server/tunnel-proxy-router.d.ts +7 -0
- package/dist/server/tunnel-proxy-router.js +110 -0
- package/dist/server/tunnel-proxy-router.js.map +1 -0
- package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
- package/dist/server/tunnel-proxy-router.test.js +229 -0
- package/dist/server/tunnel-proxy-router.test.js.map +1 -0
- package/dist/server/view-name.test-d.d.ts +1 -0
- package/dist/server/view-name.test-d.js +8 -0
- package/dist/server/view-name.test-d.js.map +1 -0
- package/dist/server/view-resource-resolution.test.d.ts +6 -0
- package/dist/server/view-resource-resolution.test.js +171 -0
- package/dist/server/view-resource-resolution.test.js.map +1 -0
- package/dist/server/viewsDevServer.d.ts +14 -0
- package/dist/server/viewsDevServer.js +45 -0
- package/dist/server/viewsDevServer.js.map +1 -0
- package/dist/test/utils.d.ts +13 -21
- package/dist/test/utils.js +42 -37
- package/dist/test/utils.js.map +1 -1
- package/dist/test/view.test.d.ts +1 -0
- package/dist/test/view.test.js +536 -0
- package/dist/test/view.test.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/web/bridges/adaptor.d.ts +51 -0
- package/dist/web/bridges/adaptor.js +330 -0
- package/dist/web/bridges/adaptor.js.map +1 -0
- package/dist/web/bridges/adaptor.test.d.ts +1 -0
- package/dist/web/bridges/adaptor.test.js +208 -0
- package/dist/web/bridges/adaptor.test.js.map +1 -0
- package/dist/web/bridges/apps-sdk/bridge.d.ts +7 -2
- package/dist/web/bridges/apps-sdk/bridge.js +15 -3
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
- package/dist/web/bridges/apps-sdk/index.d.ts +1 -2
- package/dist/web/bridges/apps-sdk/index.js +0 -1
- package/dist/web/bridges/apps-sdk/index.js.map +1 -1
- package/dist/web/bridges/apps-sdk/types.d.ts +32 -14
- package/dist/web/bridges/apps-sdk/types.js.map +1 -1
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +11 -0
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +11 -0
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
- package/dist/web/bridges/get-adaptor.d.ts +7 -0
- package/dist/web/bridges/get-adaptor.js +9 -7
- package/dist/web/bridges/get-adaptor.js.map +1 -1
- package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
- package/dist/web/bridges/get-adaptor.test.js +32 -0
- package/dist/web/bridges/get-adaptor.test.js.map +1 -0
- package/dist/web/bridges/index.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.d.ts +27 -31
- package/dist/web/bridges/mcp-app/bridge.js +109 -198
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
- package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
- package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
- package/dist/web/bridges/mcp-app/index.d.ts +0 -1
- package/dist/web/bridges/mcp-app/index.js +0 -1
- package/dist/web/bridges/mcp-app/index.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +17 -3
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js +14 -2
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +1 -41
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
- package/dist/web/bridges/mcp-app/view-tools.test.js +143 -0
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
- package/dist/web/bridges/types.d.ts +130 -14
- package/dist/web/bridges/types.js +14 -1
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/bridges/types.test.d.ts +1 -0
- package/dist/web/bridges/types.test.js +19 -0
- package/dist/web/bridges/types.test.js.map +1 -0
- package/dist/web/bridges/use-host-context.d.ts +5 -0
- package/dist/web/bridges/use-host-context.js +5 -0
- package/dist/web/bridges/use-host-context.js.map +1 -1
- package/dist/web/components/modal-provider.d.ts +1 -1
- package/dist/web/components/modal-provider.js +5 -6
- package/dist/web/components/modal-provider.js.map +1 -1
- package/dist/web/create-store.d.ts +26 -0
- package/dist/web/create-store.js +43 -3
- package/dist/web/create-store.js.map +1 -1
- package/dist/web/create-store.test.js +31 -25
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.d.ts +35 -2
- package/dist/web/data-llm.js +31 -3
- package/dist/web/data-llm.js.map +1 -1
- package/dist/web/data-llm.test.js +50 -35
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.d.ts +22 -18
- package/dist/web/generate-helpers.js +22 -18
- package/dist/web/generate-helpers.js.map +1 -1
- package/dist/web/generate-helpers.test-d.js +30 -28
- package/dist/web/generate-helpers.test-d.js.map +1 -1
- package/dist/web/generate-helpers.test.js.map +1 -1
- package/dist/web/helpers/state.d.ts +2 -2
- package/dist/web/helpers/state.js +11 -11
- package/dist/web/helpers/state.js.map +1 -1
- package/dist/web/helpers/state.test.js +9 -9
- package/dist/web/helpers/state.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +6 -2
- package/dist/web/hooks/index.js +5 -1
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/test/utils.d.ts +6 -2
- package/dist/web/hooks/test/utils.js +17 -2
- package/dist/web/hooks/test/utils.js.map +1 -1
- package/dist/web/hooks/use-call-tool.d.ts +45 -0
- package/dist/web/hooks/use-call-tool.js +28 -0
- package/dist/web/hooks/use-call-tool.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test-d.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +62 -27
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-display-mode.d.ts +23 -3
- package/dist/web/hooks/use-display-mode.js +20 -0
- package/dist/web/hooks/use-display-mode.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test-d.d.ts +1 -0
- package/dist/web/hooks/use-display-mode.test-d.js +8 -0
- package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
- package/dist/web/hooks/use-display-mode.test.js +56 -20
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-download.d.ts +5 -0
- package/dist/web/hooks/use-download.js +8 -0
- package/dist/web/hooks/use-download.js.map +1 -0
- package/dist/web/hooks/use-download.test.d.ts +1 -0
- package/dist/web/hooks/use-download.test.js +103 -0
- package/dist/web/hooks/use-download.test.js.map +1 -0
- package/dist/web/hooks/use-files.d.ts +34 -1
- package/dist/web/hooks/use-files.js +33 -0
- package/dist/web/hooks/use-files.js.map +1 -1
- package/dist/web/hooks/use-files.test.js +35 -3
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-layout.d.ts +2 -0
- package/dist/web/hooks/use-layout.js +2 -0
- package/dist/web/hooks/use-layout.js.map +1 -1
- package/dist/web/hooks/use-layout.test.js +62 -29
- package/dist/web/hooks/use-layout.test.js.map +1 -1
- package/dist/web/hooks/use-open-external.d.ts +20 -1
- package/dist/web/hooks/use-open-external.js +17 -1
- package/dist/web/hooks/use-open-external.js.map +1 -1
- package/dist/web/hooks/use-open-external.test.js +38 -13
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
- package/dist/web/hooks/use-register-view-tool.js +50 -0
- package/dist/web/hooks/use-register-view-tool.js.map +1 -0
- package/dist/web/hooks/use-request-close.d.ts +16 -0
- package/dist/web/hooks/use-request-close.js +21 -0
- package/dist/web/hooks/use-request-close.js.map +1 -0
- package/dist/web/hooks/use-request-close.test.d.ts +1 -0
- package/dist/web/hooks/use-request-close.test.js +47 -0
- package/dist/web/hooks/use-request-close.test.js.map +1 -0
- package/dist/web/hooks/use-request-modal.d.ts +16 -1
- package/dist/web/hooks/use-request-modal.js +19 -4
- package/dist/web/hooks/use-request-modal.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +15 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.d.ts +20 -0
- package/dist/web/hooks/use-request-size.js +24 -0
- package/dist/web/hooks/use-request-size.js.map +1 -0
- package/dist/web/hooks/use-request-size.test.d.ts +1 -0
- package/dist/web/hooks/use-request-size.test.js +47 -0
- package/dist/web/hooks/use-request-size.test.js.map +1 -0
- package/dist/web/hooks/use-send-follow-up-message.d.ts +19 -1
- package/dist/web/hooks/use-send-follow-up-message.js +19 -2
- package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.d.ts +17 -0
- package/dist/web/hooks/use-set-open-in-app-url.js +17 -0
- package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +30 -16
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.d.ts +53 -2
- package/dist/web/hooks/use-tool-info.js +30 -7
- package/dist/web/hooks/use-tool-info.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test-d.js +11 -29
- package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +43 -33
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.d.ts +2 -0
- package/dist/web/hooks/use-user.js +20 -2
- package/dist/web/hooks/use-user.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +101 -27
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.d.ts +25 -0
- package/dist/web/hooks/use-view-state.js +32 -0
- package/dist/web/hooks/use-view-state.js.map +1 -0
- package/dist/web/hooks/use-view-state.test.d.ts +1 -0
- package/dist/web/hooks/use-view-state.test.js +181 -0
- package/dist/web/hooks/use-view-state.test.js.map +1 -0
- package/dist/web/index.d.ts +1 -2
- package/dist/web/index.js +1 -2
- package/dist/web/index.js.map +1 -1
- package/dist/web/mount-view.d.ts +20 -0
- package/dist/web/{mount-widget.js → mount-view.js} +21 -2
- package/dist/web/mount-view.js.map +1 -0
- package/dist/web/plugin/data-llm.test.js.map +1 -1
- package/dist/web/plugin/plugin.d.ts +32 -1
- package/dist/web/plugin/plugin.js +167 -18
- package/dist/web/plugin/plugin.js.map +1 -1
- package/dist/web/plugin/scan-views.d.ts +16 -0
- package/dist/web/plugin/scan-views.js +88 -0
- package/dist/web/plugin/scan-views.js.map +1 -0
- package/dist/web/plugin/scan-views.test.d.ts +1 -0
- package/dist/web/plugin/scan-views.test.js +99 -0
- package/dist/web/plugin/scan-views.test.js.map +1 -0
- package/dist/web/plugin/transform-data-llm.js +1 -1
- package/dist/web/plugin/transform-data-llm.js.map +1 -1
- package/dist/web/plugin/transform-data-llm.test.js.map +1 -1
- package/dist/web/plugin/validate-view.d.ts +1 -0
- package/dist/web/plugin/validate-view.js +9 -0
- package/dist/web/plugin/validate-view.js.map +1 -0
- package/dist/web/plugin/validate-view.test.d.ts +1 -0
- package/dist/web/plugin/validate-view.test.js +24 -0
- package/dist/web/plugin/validate-view.test.js.map +1 -0
- package/dist/web/proxy.js +0 -1
- package/dist/web/proxy.js.map +1 -1
- package/dist/web/types.d.ts +4 -0
- package/dist/web/types.js.map +1 -1
- package/package.json +51 -29
- package/tsconfig.base.json +33 -0
- package/dist/server/templates/development.hbs +0 -66
- package/dist/server/templates/production.hbs +0 -7
- package/dist/server/widgetsDevServer.d.ts +0 -12
- package/dist/server/widgetsDevServer.js +0 -47
- package/dist/server/widgetsDevServer.js.map +0 -1
- package/dist/test/widget.test.js +0 -255
- package/dist/test/widget.test.js.map +0 -1
- package/dist/web/bridges/apps-sdk/adaptor.d.ts +0 -22
- package/dist/web/bridges/apps-sdk/adaptor.js +0 -64
- package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
- package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -36
- package/dist/web/bridges/mcp-app/adaptor.js +0 -185
- package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
- package/dist/web/hooks/use-widget-state.d.ts +0 -4
- package/dist/web/hooks/use-widget-state.js +0 -32
- package/dist/web/hooks/use-widget-state.js.map +0 -1
- package/dist/web/hooks/use-widget-state.test.js +0 -61
- package/dist/web/hooks/use-widget-state.test.js.map +0 -1
- package/dist/web/mount-widget.d.ts +0 -1
- package/dist/web/mount-widget.js.map +0 -1
- /package/dist/{test/widget.test.d.ts → cli/build-helpers.test.d.ts} +0 -0
- /package/dist/{web/hooks/use-widget-state.test.d.ts → cli/build-steps.test.d.ts} +0 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { startTunnelControlServer } from "../cli/tunnel-control-server.js";
|
|
7
|
+
import { createTunnelProxyRouter } from "./tunnel-proxy-router.js";
|
|
8
|
+
function makeFakeChild() {
|
|
9
|
+
const child = new EventEmitter();
|
|
10
|
+
child.stdout = new Readable({ read() { } });
|
|
11
|
+
child.stderr = new Readable({ read() { } });
|
|
12
|
+
child.kill = vi.fn(() => true);
|
|
13
|
+
return child;
|
|
14
|
+
}
|
|
15
|
+
async function listen(handler) {
|
|
16
|
+
const server = http.createServer(handler);
|
|
17
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
18
|
+
const port = server.address().port;
|
|
19
|
+
return { port, server };
|
|
20
|
+
}
|
|
21
|
+
const cleanups = [];
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
while (cleanups.length > 0) {
|
|
24
|
+
const cleanup = cleanups.pop();
|
|
25
|
+
if (cleanup) {
|
|
26
|
+
await cleanup();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
async function startProxy(controlPort) {
|
|
31
|
+
const app = express();
|
|
32
|
+
app.use(createTunnelProxyRouter(controlPort));
|
|
33
|
+
const { port, server } = await listen(app);
|
|
34
|
+
cleanups.push(() => new Promise((resolve) => {
|
|
35
|
+
server.closeAllConnections?.();
|
|
36
|
+
server.close(() => resolve());
|
|
37
|
+
}));
|
|
38
|
+
return { port, server };
|
|
39
|
+
}
|
|
40
|
+
async function startControl() {
|
|
41
|
+
const child = makeFakeChild();
|
|
42
|
+
const control = await startTunnelControlServer(() => 3000, {
|
|
43
|
+
spawn: () => child,
|
|
44
|
+
});
|
|
45
|
+
cleanups.push(() => control.close());
|
|
46
|
+
return { control, child };
|
|
47
|
+
}
|
|
48
|
+
describe("createTunnelProxyRouter", () => {
|
|
49
|
+
describe("POST /__skybridge/tunnel", () => {
|
|
50
|
+
it("forwards to upstream and returns the upstream JSON", async () => {
|
|
51
|
+
const { control } = await startControl();
|
|
52
|
+
const { port } = await startProxy(control.port);
|
|
53
|
+
const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
});
|
|
56
|
+
expect(res.status).toBe(200);
|
|
57
|
+
expect(res.headers.get("content-type")).toMatch(/application\/json/);
|
|
58
|
+
expect(await res.json()).toEqual({
|
|
59
|
+
status: "starting",
|
|
60
|
+
message: "Starting tunnel…",
|
|
61
|
+
});
|
|
62
|
+
expect(control.manager.getState().status).toBe("starting");
|
|
63
|
+
});
|
|
64
|
+
it("returns 502 when upstream is unavailable", async () => {
|
|
65
|
+
// Pick a port nothing is listening on by starting+stopping a server.
|
|
66
|
+
const probe = http.createServer();
|
|
67
|
+
await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve));
|
|
68
|
+
const deadPort = probe.address().port;
|
|
69
|
+
await new Promise((resolve) => probe.close(() => resolve()));
|
|
70
|
+
const { port } = await startProxy(deadPort);
|
|
71
|
+
const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
});
|
|
74
|
+
expect(res.status).toBe(502);
|
|
75
|
+
const body = (await res.json());
|
|
76
|
+
expect(body.status).toBe("error");
|
|
77
|
+
expect(typeof body.message).toBe("string");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("DELETE /__skybridge/tunnel", () => {
|
|
81
|
+
it("forwards to upstream and returns the upstream JSON", async () => {
|
|
82
|
+
const { control, child } = await startControl();
|
|
83
|
+
const { port } = await startProxy(control.port);
|
|
84
|
+
// First start the tunnel so DELETE has something to stop.
|
|
85
|
+
await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
});
|
|
88
|
+
const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
|
|
89
|
+
method: "DELETE",
|
|
90
|
+
});
|
|
91
|
+
expect(res.status).toBe(200);
|
|
92
|
+
expect(await res.json()).toEqual({ status: "idle" });
|
|
93
|
+
expect(child.kill).toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
it("returns 502 when upstream is unavailable", async () => {
|
|
96
|
+
const probe = http.createServer();
|
|
97
|
+
await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve));
|
|
98
|
+
const deadPort = probe.address().port;
|
|
99
|
+
await new Promise((resolve) => probe.close(() => resolve()));
|
|
100
|
+
const { port } = await startProxy(deadPort);
|
|
101
|
+
const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
|
|
102
|
+
method: "DELETE",
|
|
103
|
+
});
|
|
104
|
+
expect(res.status).toBe(502);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe("GET /__skybridge/tunnel/events", () => {
|
|
108
|
+
it("pipes the upstream SSE stream through to the client", async () => {
|
|
109
|
+
const { control, child } = await startControl();
|
|
110
|
+
const { port } = await startProxy(control.port);
|
|
111
|
+
// Get the manager into a known state so the initial SSE frame is
|
|
112
|
+
// deterministic.
|
|
113
|
+
await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
});
|
|
116
|
+
child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
|
|
117
|
+
const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel/events`);
|
|
118
|
+
expect(res.status).toBe(200);
|
|
119
|
+
expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
|
|
120
|
+
expect(res.headers.get("cache-control")).toMatch(/no-cache/);
|
|
121
|
+
const body = res.body;
|
|
122
|
+
if (!body) {
|
|
123
|
+
throw new Error("expected response body");
|
|
124
|
+
}
|
|
125
|
+
const reader = body.getReader();
|
|
126
|
+
const { value } = await reader.read();
|
|
127
|
+
const chunk = new TextDecoder().decode(value);
|
|
128
|
+
expect(chunk).toContain("event: state");
|
|
129
|
+
expect(chunk).toContain('"status":"connected"');
|
|
130
|
+
expect(chunk).toContain('"url":"https://abc.tunnel.example"');
|
|
131
|
+
await reader.cancel();
|
|
132
|
+
});
|
|
133
|
+
it("forwards subsequent state changes through the SSE stream", async () => {
|
|
134
|
+
const { control, child } = await startControl();
|
|
135
|
+
const { port } = await startProxy(control.port);
|
|
136
|
+
await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
});
|
|
139
|
+
const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel/events`);
|
|
140
|
+
const body = res.body;
|
|
141
|
+
if (!body) {
|
|
142
|
+
throw new Error("expected response body");
|
|
143
|
+
}
|
|
144
|
+
const reader = body.getReader();
|
|
145
|
+
const decoder = new TextDecoder();
|
|
146
|
+
// Drain the initial "starting" frame.
|
|
147
|
+
const first = await reader.read();
|
|
148
|
+
expect(decoder.decode(first.value)).toContain('"status":"starting"');
|
|
149
|
+
// Now drive a state change on the manager and read the next frame.
|
|
150
|
+
child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
|
|
151
|
+
let combined = "";
|
|
152
|
+
// Reads may chunk arbitrarily, so accumulate until we see the connected
|
|
153
|
+
// event or hit a sane cap.
|
|
154
|
+
for (let i = 0; i < 5; i++) {
|
|
155
|
+
const { value, done } = await reader.read();
|
|
156
|
+
if (done) {
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
combined += decoder.decode(value);
|
|
160
|
+
if (combined.includes('"status":"connected"')) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
expect(combined).toContain('"status":"connected"');
|
|
165
|
+
expect(combined).toContain('"url":"https://abc.tunnel.example"');
|
|
166
|
+
await reader.cancel();
|
|
167
|
+
});
|
|
168
|
+
it("returns 502 when upstream is unavailable", async () => {
|
|
169
|
+
const probe = http.createServer();
|
|
170
|
+
await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve));
|
|
171
|
+
const deadPort = probe.address().port;
|
|
172
|
+
await new Promise((resolve) => probe.close(() => resolve()));
|
|
173
|
+
const { port } = await startProxy(deadPort);
|
|
174
|
+
const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel/events`);
|
|
175
|
+
expect(res.status).toBe(502);
|
|
176
|
+
const body = (await res.json());
|
|
177
|
+
expect(body.status).toBe("error");
|
|
178
|
+
});
|
|
179
|
+
it("aborts the upstream connection when the proxy server is closed mid-stream", async () => {
|
|
180
|
+
const { control } = await startControl();
|
|
181
|
+
const { port, server } = await startProxy(control.port);
|
|
182
|
+
await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
});
|
|
185
|
+
// Snapshot the manager's listener counts before the SSE subscription so
|
|
186
|
+
// we can verify the proxy disconnected from upstream after shutdown.
|
|
187
|
+
const baseStateListeners = control.manager.listenerCount("state");
|
|
188
|
+
const baseActivityListeners = control.manager.listenerCount("activity");
|
|
189
|
+
const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel/events`);
|
|
190
|
+
const body = res.body;
|
|
191
|
+
if (!body) {
|
|
192
|
+
throw new Error("expected response body");
|
|
193
|
+
}
|
|
194
|
+
const reader = body.getReader();
|
|
195
|
+
// Drain the first frame to confirm the stream is live and the upstream
|
|
196
|
+
// SSE handler has subscribed to the manager.
|
|
197
|
+
await reader.read();
|
|
198
|
+
expect(control.manager.listenerCount("state")).toBe(baseStateListeners + 1);
|
|
199
|
+
// Close the proxy server, destroying in-flight responses. The proxy's
|
|
200
|
+
// req.on("close", ...) should fire and abort the upstream fetch.
|
|
201
|
+
server.closeAllConnections?.();
|
|
202
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
203
|
+
// The client-side stream is dead — drain or fail, either is acceptable.
|
|
204
|
+
try {
|
|
205
|
+
while (true) {
|
|
206
|
+
const { done } = await reader.read();
|
|
207
|
+
if (done) {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// expected: socket terminated when proxy server was destroyed
|
|
214
|
+
}
|
|
215
|
+
// Wait briefly for the upstream's req.on("close") to fire, then assert
|
|
216
|
+
// the manager listeners were detached. This is the load-bearing
|
|
217
|
+
// verification: it proves the proxy's AbortController propagated and
|
|
218
|
+
// upstream cleaned up its SSE subscription.
|
|
219
|
+
const start = Date.now();
|
|
220
|
+
while (control.manager.listenerCount("state") > baseStateListeners &&
|
|
221
|
+
Date.now() - start < 1000) {
|
|
222
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
223
|
+
}
|
|
224
|
+
expect(control.manager.listenerCount("state")).toBe(baseStateListeners);
|
|
225
|
+
expect(control.manager.listenerCount("activity")).toBe(baseActivityListeners);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
//# sourceMappingURL=tunnel-proxy-router.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-proxy-router.test.js","sourceRoot":"","sources":["../../src/server/tunnel-proxy-router.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport http from \"node:http\";\nimport { Readable } from \"node:stream\";\nimport express from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { startTunnelControlServer } from \"../cli/tunnel-control-server.js\";\nimport { createTunnelProxyRouter } from \"./tunnel-proxy-router.js\";\n\ntype FakeChild = EventEmitter & {\n stdout: Readable;\n stderr: Readable;\n kill: ReturnType<typeof vi.fn<() => boolean>>;\n};\n\nfunction makeFakeChild(): FakeChild {\n const child = new EventEmitter() as FakeChild;\n child.stdout = new Readable({ read() {} });\n child.stderr = new Readable({ read() {} });\n child.kill = vi.fn<() => boolean>(() => true);\n return child;\n}\n\nasync function listen(handler: http.RequestListener) {\n const server = http.createServer(handler);\n await new Promise<void>((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n const port = (server.address() as { port: number }).port;\n return { port, server };\n}\n\ntype Cleanup = () => Promise<void> | void;\nconst cleanups: Cleanup[] = [];\n\nafterEach(async () => {\n while (cleanups.length > 0) {\n const cleanup = cleanups.pop();\n if (cleanup) {\n await cleanup();\n }\n }\n});\n\nasync function startProxy(controlPort: number) {\n const app = express();\n app.use(createTunnelProxyRouter(controlPort));\n const { port, server } = await listen(app);\n cleanups.push(\n () =>\n new Promise<void>((resolve) => {\n server.closeAllConnections?.();\n server.close(() => resolve());\n }),\n );\n return { port, server };\n}\n\nasync function startControl() {\n const child = makeFakeChild();\n const control = await startTunnelControlServer(() => 3000, {\n spawn: () => child,\n });\n cleanups.push(() => control.close());\n return { control, child };\n}\n\ndescribe(\"createTunnelProxyRouter\", () => {\n describe(\"POST /__skybridge/tunnel\", () => {\n it(\"forwards to upstream and returns the upstream JSON\", async () => {\n const { control } = await startControl();\n const { port } = await startProxy(control.port);\n\n const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n expect(res.status).toBe(200);\n expect(res.headers.get(\"content-type\")).toMatch(/application\\/json/);\n expect(await res.json()).toEqual({\n status: \"starting\",\n message: \"Starting tunnel…\",\n });\n expect(control.manager.getState().status).toBe(\"starting\");\n });\n\n it(\"returns 502 when upstream is unavailable\", async () => {\n // Pick a port nothing is listening on by starting+stopping a server.\n const probe = http.createServer();\n await new Promise<void>((resolve) =>\n probe.listen(0, \"127.0.0.1\", resolve),\n );\n const deadPort = (probe.address() as { port: number }).port;\n await new Promise<void>((resolve) => probe.close(() => resolve()));\n\n const { port } = await startProxy(deadPort);\n\n const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n expect(res.status).toBe(502);\n const body = (await res.json()) as { status: string; message: string };\n expect(body.status).toBe(\"error\");\n expect(typeof body.message).toBe(\"string\");\n });\n });\n\n describe(\"DELETE /__skybridge/tunnel\", () => {\n it(\"forwards to upstream and returns the upstream JSON\", async () => {\n const { control, child } = await startControl();\n const { port } = await startProxy(control.port);\n\n // First start the tunnel so DELETE has something to stop.\n await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"DELETE\",\n });\n\n expect(res.status).toBe(200);\n expect(await res.json()).toEqual({ status: \"idle\" });\n expect(child.kill).toHaveBeenCalled();\n });\n\n it(\"returns 502 when upstream is unavailable\", async () => {\n const probe = http.createServer();\n await new Promise<void>((resolve) =>\n probe.listen(0, \"127.0.0.1\", resolve),\n );\n const deadPort = (probe.address() as { port: number }).port;\n await new Promise<void>((resolve) => probe.close(() => resolve()));\n\n const { port } = await startProxy(deadPort);\n\n const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"DELETE\",\n });\n expect(res.status).toBe(502);\n });\n });\n\n describe(\"GET /__skybridge/tunnel/events\", () => {\n it(\"pipes the upstream SSE stream through to the client\", async () => {\n const { control, child } = await startControl();\n const { port } = await startProxy(control.port);\n\n // Get the manager into a known state so the initial SSE frame is\n // deterministic.\n await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n child.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n\n const res = await fetch(\n `http://127.0.0.1:${port}/__skybridge/tunnel/events`,\n );\n\n expect(res.status).toBe(200);\n expect(res.headers.get(\"content-type\")).toMatch(/text\\/event-stream/);\n expect(res.headers.get(\"cache-control\")).toMatch(/no-cache/);\n\n const body = res.body;\n if (!body) {\n throw new Error(\"expected response body\");\n }\n const reader = body.getReader();\n const { value } = await reader.read();\n const chunk = new TextDecoder().decode(value);\n\n expect(chunk).toContain(\"event: state\");\n expect(chunk).toContain('\"status\":\"connected\"');\n expect(chunk).toContain('\"url\":\"https://abc.tunnel.example\"');\n\n await reader.cancel();\n });\n\n it(\"forwards subsequent state changes through the SSE stream\", async () => {\n const { control, child } = await startControl();\n const { port } = await startProxy(control.port);\n\n await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n const res = await fetch(\n `http://127.0.0.1:${port}/__skybridge/tunnel/events`,\n );\n const body = res.body;\n if (!body) {\n throw new Error(\"expected response body\");\n }\n const reader = body.getReader();\n const decoder = new TextDecoder();\n\n // Drain the initial \"starting\" frame.\n const first = await reader.read();\n expect(decoder.decode(first.value)).toContain('\"status\":\"starting\"');\n\n // Now drive a state change on the manager and read the next frame.\n child.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n\n let combined = \"\";\n // Reads may chunk arbitrarily, so accumulate until we see the connected\n // event or hit a sane cap.\n for (let i = 0; i < 5; i++) {\n const { value, done } = await reader.read();\n if (done) {\n break;\n }\n combined += decoder.decode(value);\n if (combined.includes('\"status\":\"connected\"')) {\n break;\n }\n }\n expect(combined).toContain('\"status\":\"connected\"');\n expect(combined).toContain('\"url\":\"https://abc.tunnel.example\"');\n\n await reader.cancel();\n });\n\n it(\"returns 502 when upstream is unavailable\", async () => {\n const probe = http.createServer();\n await new Promise<void>((resolve) =>\n probe.listen(0, \"127.0.0.1\", resolve),\n );\n const deadPort = (probe.address() as { port: number }).port;\n await new Promise<void>((resolve) => probe.close(() => resolve()));\n\n const { port } = await startProxy(deadPort);\n\n const res = await fetch(\n `http://127.0.0.1:${port}/__skybridge/tunnel/events`,\n );\n expect(res.status).toBe(502);\n const body = (await res.json()) as { status: string };\n expect(body.status).toBe(\"error\");\n });\n\n it(\"aborts the upstream connection when the proxy server is closed mid-stream\", async () => {\n const { control } = await startControl();\n const { port, server } = await startProxy(control.port);\n\n await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n // Snapshot the manager's listener counts before the SSE subscription so\n // we can verify the proxy disconnected from upstream after shutdown.\n const baseStateListeners = control.manager.listenerCount(\"state\");\n const baseActivityListeners = control.manager.listenerCount(\"activity\");\n\n const res = await fetch(\n `http://127.0.0.1:${port}/__skybridge/tunnel/events`,\n );\n const body = res.body;\n if (!body) {\n throw new Error(\"expected response body\");\n }\n const reader = body.getReader();\n // Drain the first frame to confirm the stream is live and the upstream\n // SSE handler has subscribed to the manager.\n await reader.read();\n expect(control.manager.listenerCount(\"state\")).toBe(\n baseStateListeners + 1,\n );\n\n // Close the proxy server, destroying in-flight responses. The proxy's\n // req.on(\"close\", ...) should fire and abort the upstream fetch.\n server.closeAllConnections?.();\n await new Promise<void>((resolve) => server.close(() => resolve()));\n\n // The client-side stream is dead — drain or fail, either is acceptable.\n try {\n while (true) {\n const { done } = await reader.read();\n if (done) {\n break;\n }\n }\n } catch {\n // expected: socket terminated when proxy server was destroyed\n }\n\n // Wait briefly for the upstream's req.on(\"close\") to fire, then assert\n // the manager listeners were detached. This is the load-bearing\n // verification: it proves the proxy's AbortController propagated and\n // upstream cleaned up its SSE subscription.\n const start = Date.now();\n while (\n control.manager.listenerCount(\"state\") > baseStateListeners &&\n Date.now() - start < 1000\n ) {\n await new Promise((r) => setTimeout(r, 20));\n }\n expect(control.manager.listenerCount(\"state\")).toBe(baseStateListeners);\n expect(control.manager.listenerCount(\"activity\")).toBe(\n baseActivityListeners,\n );\n });\n });\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { expectTypeOf, test } from "vitest";
|
|
2
|
+
test("ViewNameFor falls back to string when the registry is empty", () => {
|
|
3
|
+
expectTypeOf().toEqualTypeOf();
|
|
4
|
+
});
|
|
5
|
+
test("ViewNameFor narrows to the registered view names when populated", () => {
|
|
6
|
+
expectTypeOf().toEqualTypeOf();
|
|
7
|
+
});
|
|
8
|
+
//# sourceMappingURL=view-name.test-d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-name.test-d.js","sourceRoot":"","sources":["../../src/server/view-name.test-d.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAG5C,IAAI,CAAC,6DAA6D,EAAE,GAAG,EAAE;IACvE,YAAY,EAAqC,CAAC,aAAa,EAAU,CAAC;AAC5E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;IAC3E,YAAY,EAA4C,CAAC,aAAa,EAEnE,CAAC;AACN,CAAC,CAAC,CAAC","sourcesContent":["import { expectTypeOf, test } from \"vitest\";\nimport type { ViewNameFor } from \"./server.js\";\n\ntest(\"ViewNameFor falls back to string when the registry is empty\", () => {\n expectTypeOf<ViewNameFor<Record<never, never>>>().toEqualTypeOf<string>();\n});\n\ntest(\"ViewNameFor narrows to the registered view names when populated\", () => {\n expectTypeOf<ViewNameFor<{ home: true; about: true }>>().toEqualTypeOf<\n \"home\" | \"about\"\n >();\n});\n"]}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { __setBuildManifest, McpServer } from "./index.js";
|
|
7
|
+
vi.mock("@skybridge/devtools", () => ({
|
|
8
|
+
devtoolsStaticServer: () => ((_req, _res, next) => next()),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("./viewsDevServer.js", () => ({
|
|
11
|
+
viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
|
|
12
|
+
}));
|
|
13
|
+
// SKY-435: a view resource must resolve no matter the `?v=` query param value.
|
|
14
|
+
// The version param is a content-derived cache key for external consumers
|
|
15
|
+
// (it busts host/CDN caches when the bundle changes); it is not part of the
|
|
16
|
+
// resource's identity, so a stale, absent, or arbitrary param must still serve
|
|
17
|
+
// the underlying asset.
|
|
18
|
+
async function connect(register) {
|
|
19
|
+
const server = new McpServer({ name: "test", version: "1.0.0" });
|
|
20
|
+
register(server);
|
|
21
|
+
const client = new Client({ name: "test-client", version: "1.0.0" });
|
|
22
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
23
|
+
await server.connect(serverTransport);
|
|
24
|
+
await client.connect(clientTransport);
|
|
25
|
+
return {
|
|
26
|
+
client,
|
|
27
|
+
teardown: async () => {
|
|
28
|
+
await client.close();
|
|
29
|
+
await server.close();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
let openHttpServer;
|
|
34
|
+
afterEach(() => openHttpServer?.close());
|
|
35
|
+
// Connect over real HTTP so forwarded proxy headers reach the resource handler.
|
|
36
|
+
async function connectHttp(register, headers) {
|
|
37
|
+
const { createApp } = await import("./express.js");
|
|
38
|
+
const server = new McpServer({ name: "test", version: "1.0.0" });
|
|
39
|
+
register(server);
|
|
40
|
+
const httpServer = http.createServer();
|
|
41
|
+
await createApp({ mcpServer: server, httpServer });
|
|
42
|
+
const listening = http.createServer(server.express);
|
|
43
|
+
await new Promise((resolve) => listening.listen(0, resolve));
|
|
44
|
+
openHttpServer = listening;
|
|
45
|
+
const port = listening.address().port;
|
|
46
|
+
const client = new Client({ name: "test-client", version: "1.0.0" });
|
|
47
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`), { requestInit: { headers } });
|
|
48
|
+
await client.connect(transport);
|
|
49
|
+
return {
|
|
50
|
+
client,
|
|
51
|
+
teardown: async () => {
|
|
52
|
+
await client.close();
|
|
53
|
+
await server.close();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function registerWidget(server) {
|
|
58
|
+
server.registerTool({ name: "show", description: "show", view: { component: "widget" } }, () => ({ content: [{ type: "text", text: "ok" }] }));
|
|
59
|
+
}
|
|
60
|
+
/** Narrow a read-resource content entry to its text variant. */
|
|
61
|
+
function textContent(contents) {
|
|
62
|
+
return contents[0];
|
|
63
|
+
}
|
|
64
|
+
describe("view resource resolution (cache key)", () => {
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
delete process.env.NODE_ENV;
|
|
67
|
+
});
|
|
68
|
+
it("resolves regardless of the ?v= param and echoes the requested URI", async () => {
|
|
69
|
+
const { client, teardown } = await connect(registerWidget);
|
|
70
|
+
const { resources } = await client.listResources();
|
|
71
|
+
const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
|
|
72
|
+
// Dev build advertises no cache key.
|
|
73
|
+
expect(listed?.uri).toBe("ui://views/ext-apps/widget.html");
|
|
74
|
+
// A stale/arbitrary/absent param all resolve the same underlying asset,
|
|
75
|
+
// and the response echoes the URI the consumer asked for (never rewritten).
|
|
76
|
+
for (const uri of [
|
|
77
|
+
"ui://views/ext-apps/widget.html?v=stale123",
|
|
78
|
+
"ui://views/ext-apps/widget.html?v=whatever-else",
|
|
79
|
+
"ui://views/ext-apps/widget.html",
|
|
80
|
+
]) {
|
|
81
|
+
const { contents } = await client.readResource({ uri });
|
|
82
|
+
expect(contents).toHaveLength(1);
|
|
83
|
+
const content = textContent(contents);
|
|
84
|
+
expect(typeof content.text).toBe("string");
|
|
85
|
+
expect(content.text.length).toBeGreaterThan(0);
|
|
86
|
+
expect(content.uri).toBe(uri);
|
|
87
|
+
}
|
|
88
|
+
await teardown();
|
|
89
|
+
});
|
|
90
|
+
it("resolves a stale cache key when the canonical URI carries ?v= (prod)", async () => {
|
|
91
|
+
process.env.NODE_ENV = "production";
|
|
92
|
+
__setBuildManifest({
|
|
93
|
+
"src/views/widget.tsx": {
|
|
94
|
+
isEntry: true,
|
|
95
|
+
name: "widget",
|
|
96
|
+
file: "assets/widget-ABC123.js",
|
|
97
|
+
},
|
|
98
|
+
"style.css": { file: "assets/style-XYZ.css" },
|
|
99
|
+
});
|
|
100
|
+
const { client, teardown } = await connect(registerWidget);
|
|
101
|
+
const { resources } = await client.listResources();
|
|
102
|
+
const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
|
|
103
|
+
// Production advertises a content-derived cache key.
|
|
104
|
+
expect(listed?.uri).toMatch(/^ui:\/\/views\/ext-apps\/widget\.html\?v=[0-9a-f]{8}$/);
|
|
105
|
+
// A consumer holding a stale key still resolves the current asset.
|
|
106
|
+
const staleUri = "ui://views/ext-apps/widget.html?v=00000000";
|
|
107
|
+
const { contents } = await client.readResource({ uri: staleUri });
|
|
108
|
+
expect(contents).toHaveLength(1);
|
|
109
|
+
const content = textContent(contents);
|
|
110
|
+
expect(typeof content.text).toBe("string");
|
|
111
|
+
expect(content.uri).toBe(staleUri);
|
|
112
|
+
await teardown();
|
|
113
|
+
});
|
|
114
|
+
it("still throws for an unknown view path", async () => {
|
|
115
|
+
const { client, teardown } = await connect(registerWidget);
|
|
116
|
+
await expect(client.readResource({ uri: "ui://views/ext-apps/nope.html?v=1" })).rejects.toThrow();
|
|
117
|
+
await teardown();
|
|
118
|
+
});
|
|
119
|
+
// A proxy routing the request under a path prefix sends x-forwarded-prefix;
|
|
120
|
+
// the view's asset URLs must carry that prefix (and the forwarded origin) so
|
|
121
|
+
// they resolve back through the same routing. Per-request, so one process can
|
|
122
|
+
// serve many hosts/prefixes at once.
|
|
123
|
+
it("prefixes emitted asset URLs with x-forwarded-prefix (prod)", async () => {
|
|
124
|
+
process.env.NODE_ENV = "production";
|
|
125
|
+
__setBuildManifest({
|
|
126
|
+
"src/views/widget.tsx": {
|
|
127
|
+
isEntry: true,
|
|
128
|
+
name: "widget",
|
|
129
|
+
file: "assets/widget-ABC123.js",
|
|
130
|
+
},
|
|
131
|
+
"style.css": { file: "assets/style-XYZ.css" },
|
|
132
|
+
});
|
|
133
|
+
const { client, teardown } = await connectHttp(registerWidget, {
|
|
134
|
+
"x-forwarded-host": "foo.com",
|
|
135
|
+
"x-forwarded-proto": "https",
|
|
136
|
+
"x-forwarded-prefix": "/v1/canary",
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
const { contents } = await client.readResource({
|
|
140
|
+
uri: "ui://views/ext-apps/widget.html",
|
|
141
|
+
});
|
|
142
|
+
const { text } = textContent(contents);
|
|
143
|
+
// Forwarded origin + prefix. (`assets/assets` is the existing layout: the
|
|
144
|
+
// manifest `file` already carries `assets/` and the template prepends
|
|
145
|
+
// `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)
|
|
146
|
+
expect(text).toContain("https://foo.com/v1/canary/assets/assets/widget-ABC123.js");
|
|
147
|
+
expect(text).toContain("https://foo.com/v1/canary/assets/assets/style-XYZ.css");
|
|
148
|
+
// No unprefixed asset URL leaks through.
|
|
149
|
+
expect(text).not.toContain("foo.com/assets/");
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await teardown();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
|
|
156
|
+
// via openai/outputTemplate. We no longer advertise it, but apps published then
|
|
157
|
+
// have it cached, so the read must still resolve to the ext-apps content.
|
|
158
|
+
it("resolves the legacy apps-sdk URL to the ext-apps content", async () => {
|
|
159
|
+
const { client, teardown } = await connect(registerWidget);
|
|
160
|
+
const legacyUri = "ui://views/apps-sdk/widget.html";
|
|
161
|
+
const { contents } = await client.readResource({ uri: legacyUri });
|
|
162
|
+
expect(contents).toHaveLength(1);
|
|
163
|
+
const content = contents[0];
|
|
164
|
+
expect(content.mimeType).toBe("text/html;profile=mcp-app");
|
|
165
|
+
expect(content.text.length).toBeGreaterThan(0);
|
|
166
|
+
// The response echoes the requested (legacy) URI, never the canonical one.
|
|
167
|
+
expect(content.uri).toBe(legacyUri);
|
|
168
|
+
await teardown();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
//# sourceMappingURL=view-resource-resolution.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-resource-resolution.test.js","sourceRoot":"","sources":["../../src/server/view-resource-resolution.test.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: (_httpServer: unknown) =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\n\n// Mirror what the Skybridge Vite plugin generates in `.skybridge/views.d.ts`:\n// narrow `ViewName` so `component: \"widget\"` typechecks against the registry.\ndeclare module \"./server.js\" {\n interface ViewNameRegistry {\n widget: true;\n }\n}\n\n// SKY-435: a view resource must resolve no matter the `?v=` query param value.\n// The version param is a content-derived cache key for external consumers\n// (it busts host/CDN caches when the bundle changes); it is not part of the\n// resource's identity, so a stale, absent, or arbitrary param must still serve\n// the underlying asset.\n\nasync function connect(register: (server: McpServer) => void) {\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await server.connect(serverTransport);\n await client.connect(clientTransport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.close();\n },\n };\n}\n\nlet openHttpServer: http.Server | undefined;\nafterEach(() => openHttpServer?.close());\n\n// Connect over real HTTP so forwarded proxy headers reach the resource handler.\nasync function connectHttp(\n register: (server: McpServer) => void,\n headers: Record<string, string>,\n) {\n const { createApp } = await import(\"./express.js\");\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const httpServer = http.createServer();\n await createApp({ mcpServer: server, httpServer });\n const listening = http.createServer(server.express);\n await new Promise<void>((resolve) => listening.listen(0, resolve));\n openHttpServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const transport = new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n { requestInit: { headers } },\n );\n await client.connect(transport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.close();\n },\n };\n}\n\nfunction registerWidget(server: McpServer) {\n server.registerTool(\n { name: \"show\", description: \"show\", view: { component: \"widget\" } },\n () => ({ content: [{ type: \"text\" as const, text: \"ok\" }] }),\n );\n}\n\n/** Narrow a read-resource content entry to its text variant. */\nfunction textContent(contents: Array<{ uri: string }>): {\n uri: string;\n text: string;\n} {\n return contents[0] as { uri: string; text: string };\n}\n\ndescribe(\"view resource resolution (cache key)\", () => {\n afterEach(() => {\n delete process.env.NODE_ENV;\n });\n\n it(\"resolves regardless of the ?v= param and echoes the requested URI\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Dev build advertises no cache key.\n expect(listed?.uri).toBe(\"ui://views/ext-apps/widget.html\");\n\n // A stale/arbitrary/absent param all resolve the same underlying asset,\n // and the response echoes the URI the consumer asked for (never rewritten).\n for (const uri of [\n \"ui://views/ext-apps/widget.html?v=stale123\",\n \"ui://views/ext-apps/widget.html?v=whatever-else\",\n \"ui://views/ext-apps/widget.html\",\n ]) {\n const { contents } = await client.readResource({ uri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.text.length).toBeGreaterThan(0);\n expect(content.uri).toBe(uri);\n }\n\n await teardown();\n });\n\n it(\"resolves a stale cache key when the canonical URI carries ?v= (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Production advertises a content-derived cache key.\n expect(listed?.uri).toMatch(\n /^ui:\\/\\/views\\/ext-apps\\/widget\\.html\\?v=[0-9a-f]{8}$/,\n );\n\n // A consumer holding a stale key still resolves the current asset.\n const staleUri = \"ui://views/ext-apps/widget.html?v=00000000\";\n const { contents } = await client.readResource({ uri: staleUri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.uri).toBe(staleUri);\n\n await teardown();\n });\n\n it(\"still throws for an unknown view path\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n await expect(\n client.readResource({ uri: \"ui://views/ext-apps/nope.html?v=1\" }),\n ).rejects.toThrow();\n\n await teardown();\n });\n\n // A proxy routing the request under a path prefix sends x-forwarded-prefix;\n // the view's asset URLs must carry that prefix (and the forwarded origin) so\n // they resolve back through the same routing. Per-request, so one process can\n // serve many hosts/prefixes at once.\n it(\"prefixes emitted asset URLs with x-forwarded-prefix (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connectHttp(registerWidget, {\n \"x-forwarded-host\": \"foo.com\",\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-prefix\": \"/v1/canary\",\n });\n try {\n const { contents } = await client.readResource({\n uri: \"ui://views/ext-apps/widget.html\",\n });\n const { text } = textContent(contents);\n // Forwarded origin + prefix. (`assets/assets` is the existing layout: the\n // manifest `file` already carries `assets/` and the template prepends\n // `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/widget-ABC123.js\",\n );\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/style-XYZ.css\",\n );\n // No unprefixed asset URL leaks through.\n expect(text).not.toContain(\"foo.com/assets/\");\n } finally {\n await teardown();\n }\n });\n\n // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`\n // via openai/outputTemplate. We no longer advertise it, but apps published then\n // have it cached, so the read must still resolve to the ext-apps content.\n it(\"resolves the legacy apps-sdk URL to the ext-apps content\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const legacyUri = \"ui://views/apps-sdk/widget.html\";\n const { contents } = await client.readResource({ uri: legacyUri });\n\n expect(contents).toHaveLength(1);\n const content = contents[0] as {\n uri: string;\n text: string;\n mimeType: string;\n };\n expect(content.mimeType).toBe(\"text/html;profile=mcp-app\");\n expect(content.text.length).toBeGreaterThan(0);\n // The response echoes the requested (legacy) URI, never the canonical one.\n expect(content.uri).toBe(legacyUri);\n\n await teardown();\n });\n});\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type http from "node:http";
|
|
2
|
+
import { type Router } from "express";
|
|
3
|
+
/**
|
|
4
|
+
* Vite dev-server middleware for view assets.
|
|
5
|
+
*
|
|
6
|
+
* MUST be mounted at the Express app root so Vite can intercept
|
|
7
|
+
* `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:
|
|
8
|
+
*
|
|
9
|
+
* const app = express();
|
|
10
|
+
* if (env.NODE_ENV !== "production") {
|
|
11
|
+
* app.use(await viewsDevServer(httpServer));
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
export declare const viewsDevServer: (httpServer: http.Server) => Promise<Router>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import express, {} from "express";
|
|
4
|
+
import { assetBaseUrlTransformPlugin } from "./asset-base-url-transform-plugin.js";
|
|
5
|
+
/**
|
|
6
|
+
* Vite dev-server middleware for view assets.
|
|
7
|
+
*
|
|
8
|
+
* MUST be mounted at the Express app root so Vite can intercept
|
|
9
|
+
* `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:
|
|
10
|
+
*
|
|
11
|
+
* const app = express();
|
|
12
|
+
* if (env.NODE_ENV !== "production") {
|
|
13
|
+
* app.use(await viewsDevServer(httpServer));
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
export const viewsDevServer = async (httpServer) => {
|
|
17
|
+
const router = express.Router();
|
|
18
|
+
const { createServer, loadConfigFromFile } = await import("vite");
|
|
19
|
+
const root = process.cwd();
|
|
20
|
+
const configFile = path.join(root, "vite.config.ts");
|
|
21
|
+
const configResult = await loadConfigFromFile({ command: "serve", mode: "development" }, configFile, root);
|
|
22
|
+
const { build, preview, plugins: userPlugins = [], ...devConfig } = configResult?.config || {};
|
|
23
|
+
const vite = await createServer({
|
|
24
|
+
...devConfig,
|
|
25
|
+
// Pass `false` so Vite skips re-resolving a config file — we already
|
|
26
|
+
// loaded and spread the user's config above.
|
|
27
|
+
configFile: false,
|
|
28
|
+
appType: "custom",
|
|
29
|
+
server: {
|
|
30
|
+
allowedHosts: true,
|
|
31
|
+
middlewareMode: true,
|
|
32
|
+
hmr: {
|
|
33
|
+
server: httpServer,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
root,
|
|
37
|
+
// optimizeDeps is set by the skybridge Vite plugin (entries + include)
|
|
38
|
+
// so it can derive the view glob from `viewsDir`.
|
|
39
|
+
plugins: [...userPlugins, assetBaseUrlTransformPlugin()],
|
|
40
|
+
});
|
|
41
|
+
router.use(cors());
|
|
42
|
+
router.use("/", vite.middlewares);
|
|
43
|
+
return router;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=viewsDevServer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewsDevServer.js","sourceRoot":"","sources":["../../src/server/viewsDevServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,EAAE,EAAe,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,UAAuB,EACN,EAAE;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAErD,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,EACzC,UAAU,EACV,IAAI,CACL,CAAC;IAEF,MAAM,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EAAE,WAAW,GAAG,EAAE,EACzB,GAAG,SAAS,EACb,GAAG,YAAY,EAAE,MAAM,IAAI,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC;QAC9B,GAAG,SAAS;QACZ,qEAAqE;QACrE,6CAA6C;QAC7C,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE;YACN,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,IAAI;YACpB,GAAG,EAAE;gBACH,MAAM,EAAE,UAAU;aACnB;SACF;QACD,IAAI;QACJ,uEAAuE;QACvE,kDAAkD;QAClD,OAAO,EAAE,CAAC,GAAG,WAAW,EAAE,2BAA2B,EAAE,CAAC;KACzD,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport cors from \"cors\";\nimport express, { type Router } from \"express\";\nimport { assetBaseUrlTransformPlugin } from \"./asset-base-url-transform-plugin.js\";\n\n/**\n * Vite dev-server middleware for view assets.\n *\n * MUST be mounted at the Express app root so Vite can intercept\n * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:\n *\n * const app = express();\n * if (env.NODE_ENV !== \"production\") {\n * app.use(await viewsDevServer(httpServer));\n * }\n */\nexport const viewsDevServer = async (\n httpServer: http.Server,\n): Promise<Router> => {\n const router = express.Router();\n\n const { createServer, loadConfigFromFile } = await import(\"vite\");\n\n const root = process.cwd();\n const configFile = path.join(root, \"vite.config.ts\");\n\n const configResult = await loadConfigFromFile(\n { command: \"serve\", mode: \"development\" },\n configFile,\n root,\n );\n\n const {\n build,\n preview,\n plugins: userPlugins = [],\n ...devConfig\n } = configResult?.config || {};\n\n const vite = await createServer({\n ...devConfig,\n // Pass `false` so Vite skips re-resolving a config file — we already\n // loaded and spread the user's config above.\n configFile: false,\n appType: \"custom\",\n server: {\n allowedHosts: true,\n middlewareMode: true,\n hmr: {\n server: httpServer,\n },\n },\n root,\n // optimizeDeps is set by the skybridge Vite plugin (entries + include)\n // so it can derive the view glob from `viewsDir`.\n plugins: [...userPlugins, assetBaseUrlTransformPlugin()],\n });\n\n router.use(cors());\n router.use(\"/\", vite.middlewares);\n\n return router;\n};\n"]}
|