@hydranium/core 1.0.0-next.10 → 1.0.0-next.104
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/lib/documents/ast-document-manager.d.ts +75 -16
- package/lib/documents/ast-document-manager.d.ts.map +1 -1
- package/lib/documents/ast-document-manager.js +57 -19
- package/lib/documents/ast-document-manager.js.map +1 -1
- package/lib/documents/client-ids.d.ts +5 -19
- package/lib/documents/client-ids.d.ts.map +1 -1
- package/lib/documents/client-ids.js +5 -19
- package/lib/documents/client-ids.js.map +1 -1
- package/lib/documents/hydranium-text-documents.d.ts +124 -36
- package/lib/documents/hydranium-text-documents.d.ts.map +1 -1
- package/lib/documents/hydranium-text-documents.js +191 -51
- package/lib/documents/hydranium-text-documents.js.map +1 -1
- package/lib/documents/language-client-text-shadow.d.ts +41 -9
- package/lib/documents/language-client-text-shadow.d.ts.map +1 -1
- package/lib/documents/language-client-text-shadow.js +56 -11
- package/lib/documents/language-client-text-shadow.js.map +1 -1
- package/lib/documents/self-save-registry.d.ts +37 -6
- package/lib/documents/self-save-registry.d.ts.map +1 -1
- package/lib/documents/self-save-registry.js +28 -15
- package/lib/documents/self-save-registry.js.map +1 -1
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -0
- package/lib/index.js.map +1 -1
- package/lib/langium/ast-extension/ast-node-builder.d.ts +32 -9
- package/lib/langium/ast-extension/ast-node-builder.d.ts.map +1 -1
- package/lib/langium/ast-extension/ast-node-builder.js +60 -22
- package/lib/langium/ast-extension/ast-node-builder.js.map +1 -1
- package/lib/langium/bootstrap.d.ts.map +1 -1
- package/lib/langium/bootstrap.js +16 -9
- package/lib/langium/bootstrap.js.map +1 -1
- package/lib/langium/config/configuration-provider.d.ts +81 -0
- package/lib/langium/config/configuration-provider.d.ts.map +1 -0
- package/lib/langium/config/configuration-provider.js +112 -0
- package/lib/langium/config/configuration-provider.js.map +1 -0
- package/lib/langium/config/index.d.ts +1 -0
- package/lib/langium/config/index.d.ts.map +1 -1
- package/lib/langium/config/index.js +1 -0
- package/lib/langium/config/index.js.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.d.ts +17 -0
- package/lib/langium/diagnostics/lsp-logger.d.ts.map +1 -1
- package/lib/langium/diagnostics/lsp-logger.js +40 -6
- package/lib/langium/diagnostics/lsp-logger.js.map +1 -1
- package/lib/langium/document-builder/build-pipeline-integration.d.ts +21 -1
- package/lib/langium/document-builder/build-pipeline-integration.d.ts.map +1 -1
- package/lib/langium/document-builder/build-pipeline-integration.js +32 -5
- package/lib/langium/document-builder/build-pipeline-integration.js.map +1 -1
- package/lib/langium/document-builder/build-session.d.ts +93 -0
- package/lib/langium/document-builder/build-session.d.ts.map +1 -0
- package/lib/langium/document-builder/build-session.js +72 -0
- package/lib/langium/document-builder/build-session.js.map +1 -0
- package/lib/langium/document-builder/document-builder.d.ts +150 -7
- package/lib/langium/document-builder/document-builder.d.ts.map +1 -1
- package/lib/langium/document-builder/document-builder.js +298 -11
- package/lib/langium/document-builder/document-builder.js.map +1 -1
- package/lib/langium/document-builder/index.d.ts +1 -0
- package/lib/langium/document-builder/index.d.ts.map +1 -1
- package/lib/langium/document-builder/index.js +1 -0
- package/lib/langium/document-builder/index.js.map +1 -1
- package/lib/langium/integration-services.d.ts +45 -1
- package/lib/langium/integration-services.d.ts.map +1 -1
- package/lib/langium/integration-services.js +10 -5
- package/lib/langium/integration-services.js.map +1 -1
- package/lib/langium/integrity/integrity-service.d.ts +16 -0
- package/lib/langium/integrity/integrity-service.d.ts.map +1 -1
- package/lib/langium/integrity/integrity-service.js +110 -2
- package/lib/langium/integrity/integrity-service.js.map +1 -1
- package/lib/langium/keys/containment.d.ts +64 -0
- package/lib/langium/keys/containment.d.ts.map +1 -0
- package/lib/langium/keys/containment.js +53 -0
- package/lib/langium/keys/containment.js.map +1 -0
- package/lib/langium/keys/index.d.ts +1 -0
- package/lib/langium/keys/index.d.ts.map +1 -1
- package/lib/langium/keys/index.js +1 -0
- package/lib/langium/keys/index.js.map +1 -1
- package/lib/langium/keys/name-based-key-provider.d.ts +4 -0
- package/lib/langium/keys/name-based-key-provider.d.ts.map +1 -1
- package/lib/langium/keys/name-based-key-provider.js +4 -0
- package/lib/langium/keys/name-based-key-provider.js.map +1 -1
- package/lib/langium/language-module.d.ts +25 -0
- package/lib/langium/language-module.d.ts.map +1 -1
- package/lib/langium/language-module.js +14 -0
- package/lib/langium/language-module.js.map +1 -1
- package/lib/langium/model-service/model-service.d.ts +161 -10
- package/lib/langium/model-service/model-service.d.ts.map +1 -1
- package/lib/langium/model-service/model-service.js +174 -95
- package/lib/langium/model-service/model-service.js.map +1 -1
- package/lib/langium/module.d.ts +84 -15
- package/lib/langium/module.d.ts.map +1 -1
- package/lib/langium/module.js +26 -12
- package/lib/langium/module.js.map +1 -1
- package/lib/langium/naming/name-provider.d.ts +25 -4
- package/lib/langium/naming/name-provider.d.ts.map +1 -1
- package/lib/langium/naming/name-provider.js +2 -1
- package/lib/langium/naming/name-provider.js.map +1 -1
- package/lib/langium/naming/name-separator-validation.d.ts +23 -0
- package/lib/langium/naming/name-separator-validation.d.ts.map +1 -1
- package/lib/langium/naming/name-separator-validation.js +30 -1
- package/lib/langium/naming/name-separator-validation.js.map +1 -1
- package/lib/langium/residency/cst-residency-service.d.ts +31 -11
- package/lib/langium/residency/cst-residency-service.d.ts.map +1 -1
- package/lib/langium/residency/cst-residency-service.js +13 -56
- package/lib/langium/residency/cst-residency-service.js.map +1 -1
- package/lib/langium/scope/hydranium-scope-provider.d.ts +48 -13
- package/lib/langium/scope/hydranium-scope-provider.d.ts.map +1 -1
- package/lib/langium/scope/hydranium-scope-provider.js +56 -20
- package/lib/langium/scope/hydranium-scope-provider.js.map +1 -1
- package/lib/langium/serialization/abstract-serializer.d.ts +11 -20
- package/lib/langium/serialization/abstract-serializer.d.ts.map +1 -1
- package/lib/langium/serialization/abstract-serializer.js +12 -21
- package/lib/langium/serialization/abstract-serializer.js.map +1 -1
- package/lib/langium/service-registry.d.ts +15 -1
- package/lib/langium/service-registry.d.ts.map +1 -1
- package/lib/langium/service-registry.js +16 -0
- package/lib/langium/service-registry.js.map +1 -1
- package/lib/langium/shared-services.d.ts +15 -2
- package/lib/langium/shared-services.d.ts.map +1 -1
- package/lib/langium/shared-services.js.map +1 -1
- package/lib/langium/transfer/transfer-encoder.d.ts +60 -17
- package/lib/langium/transfer/transfer-encoder.d.ts.map +1 -1
- package/lib/langium/transfer/transfer-encoder.js +38 -16
- package/lib/langium/transfer/transfer-encoder.js.map +1 -1
- package/lib/langium/trivia/comment-preserver.d.ts +423 -0
- package/lib/langium/trivia/comment-preserver.d.ts.map +1 -0
- package/lib/langium/trivia/comment-preserver.js +906 -0
- package/lib/langium/trivia/comment-preserver.js.map +1 -0
- package/lib/langium/trivia/document-ending-preserver.d.ts +43 -0
- package/lib/langium/trivia/document-ending-preserver.d.ts.map +1 -0
- package/lib/langium/trivia/document-ending-preserver.js +48 -0
- package/lib/langium/trivia/document-ending-preserver.js.map +1 -0
- package/lib/langium/trivia/index.d.ts +14 -0
- package/lib/langium/trivia/index.d.ts.map +1 -0
- package/lib/langium/trivia/index.js +14 -0
- package/lib/langium/trivia/index.js.map +1 -0
- package/lib/langium/trivia/trivia-contribution.d.ts +37 -0
- package/lib/langium/trivia/trivia-contribution.d.ts.map +1 -0
- package/lib/langium/trivia/trivia-contribution.js +10 -0
- package/lib/langium/trivia/trivia-contribution.js.map +1 -0
- package/lib/langium/trivia/trivia-preserver.d.ts +50 -0
- package/lib/langium/trivia/trivia-preserver.d.ts.map +1 -0
- package/lib/langium/trivia/trivia-preserver.js +10 -0
- package/lib/langium/trivia/trivia-preserver.js.map +1 -0
- package/lib/langium/trivia/trivia-service.d.ts +70 -0
- package/lib/langium/trivia/trivia-service.d.ts.map +1 -0
- package/lib/langium/trivia/trivia-service.js +56 -0
- package/lib/langium/trivia/trivia-service.js.map +1 -0
- package/lib/langium/validation/document-validator.d.ts +294 -10
- package/lib/langium/validation/document-validator.d.ts.map +1 -1
- package/lib/langium/validation/document-validator.js +448 -5
- package/lib/langium/validation/document-validator.js.map +1 -1
- package/lib/langium/workspace/document-uri-policy.d.ts +3 -4
- package/lib/langium/workspace/document-uri-policy.d.ts.map +1 -1
- package/lib/langium/workspace/document-uri-policy.js +3 -4
- package/lib/langium/workspace/document-uri-policy.js.map +1 -1
- package/lib/langium/workspace/file-system-provider.d.ts +9 -1
- package/lib/langium/workspace/file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/file-system-provider.js +40 -7
- package/lib/langium/workspace/file-system-provider.js.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts +25 -0
- package/lib/langium/workspace/hydranium-langium-document-factory.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-langium-document-factory.js +25 -0
- package/lib/langium/workspace/hydranium-langium-document-factory.js.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts +21 -1
- package/lib/langium/workspace/hydranium-workspace-manager.d.ts.map +1 -1
- package/lib/langium/workspace/hydranium-workspace-manager.js +28 -0
- package/lib/langium/workspace/hydranium-workspace-manager.js.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts +23 -0
- package/lib/langium/workspace/in-memory-file-system-provider.d.ts.map +1 -1
- package/lib/langium/workspace/in-memory-file-system-provider.js +47 -9
- package/lib/langium/workspace/in-memory-file-system-provider.js.map +1 -1
- package/lib/langium/workspace/initialize-workspace.d.ts +22 -2
- package/lib/langium/workspace/initialize-workspace.d.ts.map +1 -1
- package/lib/langium/workspace/initialize-workspace.js +13 -5
- package/lib/langium/workspace/initialize-workspace.js.map +1 -1
- package/lib/langium/workspace/langium-documents.d.ts +79 -17
- package/lib/langium/workspace/langium-documents.d.ts.map +1 -1
- package/lib/langium/workspace/langium-documents.js +83 -28
- package/lib/langium/workspace/langium-documents.js.map +1 -1
- package/lib/langium/workspace/virtual-document.d.ts +18 -3
- package/lib/langium/workspace/virtual-document.d.ts.map +1 -1
- package/lib/langium/workspace/virtual-document.js +19 -2
- package/lib/langium/workspace/virtual-document.js.map +1 -1
- package/lib/locale/index.d.ts +10 -0
- package/lib/locale/index.d.ts.map +1 -0
- package/lib/locale/index.js +10 -0
- package/lib/locale/index.js.map +1 -0
- package/lib/locale/server-locale.d.ts +73 -0
- package/lib/locale/server-locale.d.ts.map +1 -0
- package/lib/locale/server-locale.js +61 -0
- package/lib/locale/server-locale.js.map +1 -0
- package/lib/lsp/hydranium-document-update-handler.js +1 -1
- package/lib/lsp/hydranium-document-update-handler.js.map +1 -1
- package/lib/lsp/index.d.ts +1 -1
- package/lib/lsp/index.d.ts.map +1 -1
- package/lib/lsp/index.js +1 -1
- package/lib/lsp/index.js.map +1 -1
- package/lib/lsp/lsp-latency.d.ts +39 -0
- package/lib/lsp/lsp-latency.d.ts.map +1 -0
- package/lib/lsp/lsp-latency.js +58 -0
- package/lib/lsp/lsp-latency.js.map +1 -0
- package/lib/lsp/semantic-token-provider.d.ts +34 -1
- package/lib/lsp/semantic-token-provider.d.ts.map +1 -1
- package/lib/lsp/semantic-token-provider.js +55 -8
- package/lib/lsp/semantic-token-provider.js.map +1 -1
- package/lib/messages/carriers.d.ts +54 -0
- package/lib/messages/carriers.d.ts.map +1 -0
- package/lib/messages/carriers.js +62 -0
- package/lib/messages/carriers.js.map +1 -0
- package/lib/messages/index.d.ts +25 -0
- package/lib/messages/index.d.ts.map +1 -0
- package/lib/messages/index.js +25 -0
- package/lib/messages/index.js.map +1 -0
- package/lib/messages/renderer.d.ts +136 -0
- package/lib/messages/renderer.d.ts.map +1 -0
- package/lib/messages/renderer.js +159 -0
- package/lib/messages/renderer.js.map +1 -0
- package/lib/node/heap-ceiling.d.ts +61 -0
- package/lib/node/heap-ceiling.d.ts.map +1 -0
- package/lib/node/heap-ceiling.js +72 -0
- package/lib/node/heap-ceiling.js.map +1 -0
- package/lib/node/index.d.ts +1 -0
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +1 -0
- package/lib/node/index.js.map +1 -1
- package/lib/node/latency-from-env.d.ts +1 -1
- package/lib/node/latency-from-env.js +1 -1
- package/lib/node/node-file-system-provider.d.ts +35 -1
- package/lib/node/node-file-system-provider.d.ts.map +1 -1
- package/lib/node/node-file-system-provider.js +140 -45
- package/lib/node/node-file-system-provider.js.map +1 -1
- package/lib/node/profile-capture.d.ts +8 -2
- package/lib/node/profile-capture.d.ts.map +1 -1
- package/lib/node/profile-capture.js.map +1 -1
- package/lib/node/profiling-run.d.ts +2 -2
- package/lib/node/profiling-run.js +1 -1
- package/lib/node/profiling-run.js.map +1 -1
- package/lib/node/rename-over-open-readers.d.ts +52 -0
- package/lib/node/rename-over-open-readers.d.ts.map +1 -0
- package/lib/node/rename-over-open-readers.js +66 -0
- package/lib/node/rename-over-open-readers.js.map +1 -0
- package/lib/node/socket-launcher.d.ts +15 -0
- package/lib/node/socket-launcher.d.ts.map +1 -1
- package/lib/node/socket-launcher.js +3 -2
- package/lib/node/socket-launcher.js.map +1 -1
- package/lib/testing/fake-document.d.ts +16 -4
- package/lib/testing/fake-document.d.ts.map +1 -1
- package/lib/testing/fake-document.js +18 -3
- package/lib/testing/fake-document.js.map +1 -1
- package/lib/testing/make-noop-shared-services.d.ts +14 -0
- package/lib/testing/make-noop-shared-services.d.ts.map +1 -1
- package/lib/testing/make-noop-shared-services.js +28 -2
- package/lib/testing/make-noop-shared-services.js.map +1 -1
- package/lib/testing/make-test-services.d.ts +44 -8
- package/lib/testing/make-test-services.d.ts.map +1 -1
- package/lib/testing/make-test-services.js +19 -3
- package/lib/testing/make-test-services.js.map +1 -1
- package/lib/testing/node/scratch-workspace.d.ts.map +1 -1
- package/lib/testing/node/scratch-workspace.js +9 -4
- package/lib/testing/node/scratch-workspace.js.map +1 -1
- package/lib/testing/node/spawned-server.d.ts +14 -0
- package/lib/testing/node/spawned-server.d.ts.map +1 -1
- package/lib/testing/node/spawned-server.js +33 -1
- package/lib/testing/node/spawned-server.js.map +1 -1
- package/lib/testing/playwright/flaky-network-proxy.d.ts +76 -0
- package/lib/testing/playwright/flaky-network-proxy.d.ts.map +1 -0
- package/lib/testing/playwright/flaky-network-proxy.js +164 -0
- package/lib/testing/playwright/flaky-network-proxy.js.map +1 -0
- package/lib/testing/playwright/index.d.ts +1 -0
- package/lib/testing/playwright/index.d.ts.map +1 -1
- package/lib/testing/playwright/index.js +1 -0
- package/lib/testing/playwright/index.js.map +1 -1
- package/lib/testing/playwright/server-log-capture.d.ts +14 -4
- package/lib/testing/playwright/server-log-capture.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-capture.js +21 -4
- package/lib/testing/playwright/server-log-capture.js.map +1 -1
- package/lib/testing/playwright/server-log-rename-reporter.d.ts.map +1 -1
- package/lib/testing/playwright/server-log-rename-reporter.js +10 -1
- package/lib/testing/playwright/server-log-rename-reporter.js.map +1 -1
- package/lib/testing/stub-ast-document-manager.d.ts +5 -5
- package/lib/testing/stub-ast-document-manager.d.ts.map +1 -1
- package/lib/testing/stub-ast-document-manager.js +4 -6
- package/lib/testing/stub-ast-document-manager.js.map +1 -1
- package/lib/testing/stub-document-builder.d.ts +8 -0
- package/lib/testing/stub-document-builder.d.ts.map +1 -1
- package/lib/testing/stub-document-builder.js +22 -3
- package/lib/testing/stub-document-builder.js.map +1 -1
- package/lib/testing/stub-langium-documents.d.ts +3 -2
- package/lib/testing/stub-langium-documents.d.ts.map +1 -1
- package/lib/testing/stub-langium-documents.js.map +1 -1
- package/lib/testing/stub-model-service.d.ts +5 -4
- package/lib/testing/stub-model-service.d.ts.map +1 -1
- package/lib/testing/stub-model-service.js +2 -2
- package/lib/testing/stub-model-service.js.map +1 -1
- package/package.json +17 -8
- package/src/documents/ast-document-manager.ts +129 -36
- package/src/documents/client-ids.ts +5 -22
- package/src/documents/hydranium-text-documents.ts +248 -75
- package/src/documents/language-client-text-shadow.ts +59 -11
- package/src/documents/self-save-registry.ts +52 -13
- package/src/index.ts +6 -0
- package/src/langium/ast-extension/ast-node-builder.ts +66 -22
- package/src/langium/bootstrap.ts +16 -9
- package/src/langium/config/configuration-provider.ts +119 -0
- package/src/langium/config/index.ts +1 -0
- package/src/langium/diagnostics/lsp-logger.ts +41 -6
- package/src/langium/document-builder/build-pipeline-integration.ts +50 -5
- package/src/langium/document-builder/build-session.ts +87 -0
- package/src/langium/document-builder/document-builder.ts +339 -11
- package/src/langium/document-builder/index.ts +1 -0
- package/src/langium/integration-services.ts +67 -6
- package/src/langium/integrity/integrity-service.ts +116 -2
- package/src/langium/keys/containment.ts +87 -0
- package/src/langium/keys/index.ts +1 -0
- package/src/langium/keys/name-based-key-provider.ts +4 -0
- package/src/langium/language-module.ts +38 -0
- package/src/langium/model-service/model-service.ts +264 -32
- package/src/langium/module.ts +106 -21
- package/src/langium/naming/name-provider.ts +27 -5
- package/src/langium/naming/name-separator-validation.ts +34 -5
- package/src/langium/residency/cst-residency-service.ts +36 -14
- package/src/langium/scope/hydranium-scope-provider.ts +60 -22
- package/src/langium/serialization/abstract-serializer.ts +13 -32
- package/src/langium/service-registry.ts +19 -2
- package/src/langium/shared-services.ts +19 -2
- package/src/langium/transfer/transfer-encoder.ts +85 -24
- package/src/langium/trivia/comment-preserver.ts +1100 -0
- package/src/langium/trivia/document-ending-preserver.ts +55 -0
- package/src/langium/trivia/index.ts +14 -0
- package/src/langium/trivia/trivia-contribution.ts +39 -0
- package/src/langium/trivia/trivia-preserver.ts +54 -0
- package/src/langium/trivia/trivia-service.ts +99 -0
- package/src/langium/validation/document-validator.ts +537 -11
- package/src/langium/workspace/document-uri-policy.ts +3 -4
- package/src/langium/workspace/file-system-provider.ts +48 -8
- package/src/langium/workspace/hydranium-langium-document-factory.ts +34 -0
- package/src/langium/workspace/hydranium-workspace-manager.ts +30 -1
- package/src/langium/workspace/in-memory-file-system-provider.ts +53 -10
- package/src/langium/workspace/initialize-workspace.ts +34 -5
- package/src/langium/workspace/langium-documents.ts +106 -33
- package/src/langium/workspace/virtual-document.ts +21 -3
- package/src/locale/index.ts +10 -0
- package/src/locale/server-locale.ts +86 -0
- package/src/lsp/hydranium-document-update-handler.ts +1 -1
- package/src/lsp/index.ts +1 -1
- package/src/lsp/lsp-latency.ts +62 -0
- package/src/lsp/semantic-token-provider.ts +74 -7
- package/src/messages/carriers.ts +78 -0
- package/src/messages/index.ts +36 -0
- package/src/messages/renderer.ts +204 -0
- package/src/node/heap-ceiling.ts +108 -0
- package/src/node/index.ts +1 -0
- package/src/node/latency-from-env.ts +1 -1
- package/src/node/node-file-system-provider.ts +154 -47
- package/src/node/profile-capture.ts +8 -2
- package/src/node/profiling-run.ts +3 -3
- package/src/node/rename-over-open-readers.ts +78 -0
- package/src/node/socket-launcher.ts +18 -2
- package/src/testing/fake-document.ts +24 -5
- package/src/testing/make-noop-shared-services.ts +50 -2
- package/src/testing/make-test-services.ts +73 -18
- package/src/testing/node/scratch-workspace.ts +9 -4
- package/src/testing/node/spawned-server.ts +35 -1
- package/src/testing/playwright/flaky-network-proxy.ts +240 -0
- package/src/testing/playwright/index.ts +1 -0
- package/src/testing/playwright/server-log-capture.ts +24 -5
- package/src/testing/playwright/server-log-rename-reporter.ts +12 -1
- package/src/testing/stub-ast-document-manager.ts +10 -14
- package/src/testing/stub-document-builder.ts +32 -3
- package/src/testing/stub-langium-documents.ts +3 -2
- package/src/testing/stub-model-service.ts +7 -6
- package/lib/lsp/instrument-connection.d.ts +0 -36
- package/lib/lsp/instrument-connection.d.ts.map +0 -1
- package/lib/lsp/instrument-connection.js +0 -62
- package/lib/lsp/instrument-connection.js.map +0 -1
- package/src/lsp/instrument-connection.ts +0 -66
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { type Clock, DefaultTracer, type Logger, NoopLogger, SystemClock, type Tracer } from '@hydranium/protocol';
|
|
11
11
|
import type { ServerSharedServicesMinimal } from '../langium/shared-services.js';
|
|
12
|
+
import { DefaultServerLocale, type ServerLocale } from '../locale/server-locale.js';
|
|
13
|
+
import { DefaultMessageRenderer, type MessageRenderer } from '../messages/renderer.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Overrides for {@link makeNoopSharedServices}. The three observability slots
|
|
@@ -29,6 +31,18 @@ export interface NoopSharedServicesOverrides {
|
|
|
29
31
|
Logger?: Logger;
|
|
30
32
|
/** Bound on the top-level `Tracer` slot. Default: a `DefaultTracer` over `Logger` + `Clock`. */
|
|
31
33
|
Tracer?: Tracer;
|
|
34
|
+
/** Factory for the `ServerLocale` slot. Default: a real one, reporting no locale. */
|
|
35
|
+
ServerLocale?: (services: ServerSharedServicesMinimal) => ServerLocale;
|
|
36
|
+
/**
|
|
37
|
+
* Factory for the `MessageRenderer` slot. Default: the real
|
|
38
|
+
* {@link DefaultMessageRenderer}, whose no-catalogue behaviour is a
|
|
39
|
+
* pass-through — so the default is the framework's own behaviour, not a stub.
|
|
40
|
+
*
|
|
41
|
+
* A factory rather than an instance, because a renderer reads the tree it is
|
|
42
|
+
* bound into. Both slots are resolved lazily, so an override is honoured
|
|
43
|
+
* however late the caller reads them.
|
|
44
|
+
*/
|
|
45
|
+
MessageRenderer?: (services: ServerSharedServicesMinimal) => MessageRenderer;
|
|
32
46
|
/** Per-slot `workspace` overrides. Slots left out resolve to `undefined`. */
|
|
33
47
|
workspace?: Record<string, unknown>;
|
|
34
48
|
/** Any other minimal slot (`ServiceRegistry`, `AstReflection`, `additionalDocuments`, …). */
|
|
@@ -63,11 +77,21 @@ export interface NoopSharedServicesOverrides {
|
|
|
63
77
|
export function makeNoopSharedServices<T extends ServerSharedServicesMinimal = ServerSharedServicesMinimal>(
|
|
64
78
|
overrides: NoopSharedServicesOverrides = {}
|
|
65
79
|
): T {
|
|
66
|
-
const {
|
|
80
|
+
const {
|
|
81
|
+
Clock: clock,
|
|
82
|
+
Logger: logger,
|
|
83
|
+
Tracer: tracer,
|
|
84
|
+
ServerLocale: serverLocale,
|
|
85
|
+
MessageRenderer: messageRenderer,
|
|
86
|
+
workspace,
|
|
87
|
+
additionalDocuments,
|
|
88
|
+
...restTop
|
|
89
|
+
} = overrides;
|
|
67
90
|
const resolvedClock = clock ?? new SystemClock();
|
|
68
91
|
const resolvedLogger = logger ?? new NoopLogger();
|
|
69
92
|
const resolvedTracer = tracer ?? new DefaultTracer(resolvedLogger, resolvedClock);
|
|
70
|
-
|
|
93
|
+
// Assembled in two steps, because both defaults read the tree they belong to.
|
|
94
|
+
const services = {
|
|
71
95
|
Clock: resolvedClock,
|
|
72
96
|
Logger: resolvedLogger,
|
|
73
97
|
Tracer: resolvedTracer,
|
|
@@ -75,4 +99,28 @@ export function makeNoopSharedServices<T extends ServerSharedServicesMinimal = S
|
|
|
75
99
|
...restTop,
|
|
76
100
|
workspace: { ...(workspace ?? {}) }
|
|
77
101
|
} as unknown as T;
|
|
102
|
+
// Lazy, like Langium's own per-slot construction — and load-bearing here:
|
|
103
|
+
// both services emit an `instantiated` trace, so building them eagerly puts
|
|
104
|
+
// two lines into the capture of every test that passes a capturing logger
|
|
105
|
+
// and asserts on emptiness.
|
|
106
|
+
defineLazySlot(services, 'ServerLocale', () => serverLocale?.(services) ?? new DefaultServerLocale(services));
|
|
107
|
+
defineLazySlot(services, 'MessageRenderer', () => messageRenderer?.(services) ?? new DefaultMessageRenderer(services));
|
|
108
|
+
return services;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Install `name` as a memoized getter, so nothing is constructed until it is read. */
|
|
112
|
+
function defineLazySlot(target: object, name: string, create: () => unknown): void {
|
|
113
|
+
let resolved: unknown;
|
|
114
|
+
let built = false;
|
|
115
|
+
Object.defineProperty(target, name, {
|
|
116
|
+
configurable: true,
|
|
117
|
+
enumerable: true,
|
|
118
|
+
get: () => {
|
|
119
|
+
if (!built) {
|
|
120
|
+
resolved = create();
|
|
121
|
+
built = true;
|
|
122
|
+
}
|
|
123
|
+
return resolved;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
78
126
|
}
|
|
@@ -18,11 +18,14 @@ import {
|
|
|
18
18
|
type TransferDiagnostic,
|
|
19
19
|
type TransferElement
|
|
20
20
|
} from '@hydranium/protocol';
|
|
21
|
+
import { DefaultServerLocale, type ServerLocale } from '../locale/server-locale.js';
|
|
22
|
+
import { DefaultMessageRenderer, type MessageRenderer } from '../messages/renderer.js';
|
|
21
23
|
import type { Harness } from '@hydranium/protocol/testing';
|
|
22
24
|
import { type AstNode, type AstNodeDescription, type WorkspaceLock } from '@hydranium/langium';
|
|
23
25
|
import type { ModelService, ModelServiceOptions } from '../langium/model-service/model-service.js';
|
|
24
26
|
import type { ServerSharedServices } from '../langium/module.js';
|
|
25
|
-
import {
|
|
27
|
+
import type { AstDiagnostic } from '../langium/validation/document-validator.js';
|
|
28
|
+
import { DefaultTransferEncoder, type TransferEncoder } from '../langium/transfer/transfer-encoder.js';
|
|
26
29
|
import { DefaultDocumentUriPolicy, type DocumentUriPolicy } from '../langium/workspace/document-uri-policy.js';
|
|
27
30
|
import { HydraniumWorkspaceLock } from '../langium/workspace/hydranium-workspace-lock.js';
|
|
28
31
|
import type { FakeDocumentOptions } from './fake-document.js';
|
|
@@ -46,12 +49,21 @@ import { makeStubWritableFileSystem, type StubWritableFileSystem } from './stub-
|
|
|
46
49
|
* to `ServerSharedServices` in {@link makeTestServices} is therefore the
|
|
47
50
|
* single named place where the "stub stands in for the full service tree"
|
|
48
51
|
* assertion happens.
|
|
52
|
+
*
|
|
53
|
+
* **`TDiagnostic` and `TTransferDiagnostic` are the two ends of a conversion
|
|
54
|
+
* and must stay separate.** The first is what the build left on
|
|
55
|
+
* `LangiumDocument.diagnostics` and what `ModelService` hands back on an
|
|
56
|
+
* `AstDocument`; the second is what `TransferEncoder.toTransferDiagnostic`
|
|
57
|
+
* produces for the wire. One parameter serving both makes the tree
|
|
58
|
+
* unbindable for an adopter who extends either shape, since extending one
|
|
59
|
+
* says nothing about the other.
|
|
49
60
|
*/
|
|
50
61
|
export interface TestSharedServices<
|
|
51
62
|
TAst extends AstNode = AstNode,
|
|
52
|
-
TDiagnostic extends
|
|
63
|
+
TDiagnostic extends AstDiagnostic = AstDiagnostic,
|
|
53
64
|
TTransfer extends TransferElement = TransferElement,
|
|
54
|
-
TProject extends Project = Project
|
|
65
|
+
TProject extends Project = Project,
|
|
66
|
+
TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
|
|
55
67
|
> {
|
|
56
68
|
readonly Clock: Clock;
|
|
57
69
|
readonly Logger: Logger;
|
|
@@ -90,17 +102,27 @@ export interface TestSharedServices<
|
|
|
90
102
|
WorkspaceLock: WorkspaceLock;
|
|
91
103
|
};
|
|
92
104
|
readonly model: {
|
|
93
|
-
TransferEncoder: TransferEncoder<
|
|
105
|
+
TransferEncoder: TransferEncoder<TTransferDiagnostic>;
|
|
94
106
|
ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
|
|
95
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* The REAL services, not stubs — the framework renderer's no-catalogue
|
|
110
|
+
* behaviour is a pass-through, so the stub tree agrees with a production one
|
|
111
|
+
* unless a test installs a catalogue. Bound rather than omitted because
|
|
112
|
+
* `HydraniumDocumentBuilder` renders through the renderer on every
|
|
113
|
+
* `Validated` phase, where an omitted slot is a `TypeError`.
|
|
114
|
+
*/
|
|
115
|
+
readonly ServerLocale: ServerLocale;
|
|
116
|
+
readonly MessageRenderer: MessageRenderer;
|
|
96
117
|
}
|
|
97
118
|
|
|
98
119
|
/** Optional configuration for {@link makeTestServices}. */
|
|
99
120
|
export interface MakeTestServicesOptions<
|
|
100
121
|
TAst extends AstNode = AstNode,
|
|
101
|
-
TDiagnostic extends
|
|
122
|
+
TDiagnostic extends AstDiagnostic = AstDiagnostic,
|
|
102
123
|
TTransfer extends TransferElement = TransferElement,
|
|
103
|
-
TProject extends Project = Project
|
|
124
|
+
TProject extends Project = Project,
|
|
125
|
+
TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
|
|
104
126
|
> {
|
|
105
127
|
/**
|
|
106
128
|
* Serialiser used by the bundled `StubModelService`. The stub tree
|
|
@@ -112,6 +134,14 @@ export interface MakeTestServicesOptions<
|
|
|
112
134
|
* for tests that don't assert on the produced text.
|
|
113
135
|
*/
|
|
114
136
|
serialize?: (uri: string, root: TTransfer) => string;
|
|
137
|
+
/**
|
|
138
|
+
* Renderer bound on `MessageRenderer`. Default: the framework's own, which
|
|
139
|
+
* passes every sentence through unchanged. Supply one to install a catalogue
|
|
140
|
+
* or to drive the throwing path.
|
|
141
|
+
*/
|
|
142
|
+
messageRenderer?: (services: ServerSharedServices<TProject>) => MessageRenderer;
|
|
143
|
+
/** Locale handed to the bundle's {@link ServerLocale}. Default: none, i.e. the framework's English. */
|
|
144
|
+
locale?: string;
|
|
115
145
|
/**
|
|
116
146
|
* Languages to register on a {@link StubServiceRegistry} bound on the
|
|
117
147
|
* `ServiceRegistry` slot. This is how a test gets multi-language routing:
|
|
@@ -187,7 +217,7 @@ export interface MakeTestServicesOptions<
|
|
|
187
217
|
* Override the {@link TransferEncoder} factory. Default: framework
|
|
188
218
|
* {@link TransferEncoder} with no overrides.
|
|
189
219
|
*/
|
|
190
|
-
transferEncoder?: (services: ServerSharedServices<TProject>) => TransferEncoder<
|
|
220
|
+
transferEncoder?: (services: ServerSharedServices<TProject>) => TransferEncoder<TTransferDiagnostic>;
|
|
191
221
|
}
|
|
192
222
|
|
|
193
223
|
/**
|
|
@@ -205,9 +235,10 @@ export interface MakeTestServicesOptions<
|
|
|
205
235
|
*/
|
|
206
236
|
export interface TestServicesBundle<
|
|
207
237
|
TAst extends AstNode = AstNode,
|
|
208
|
-
TDiagnostic extends
|
|
238
|
+
TDiagnostic extends AstDiagnostic = AstDiagnostic,
|
|
209
239
|
TTransfer extends TransferElement = TransferElement,
|
|
210
|
-
TProject extends Project = Project
|
|
240
|
+
TProject extends Project = Project,
|
|
241
|
+
TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
|
|
211
242
|
> extends Harness {
|
|
212
243
|
readonly services: ServerSharedServices<TProject>;
|
|
213
244
|
readonly documents: StubLangiumDocuments<TAst, TDiagnostic>;
|
|
@@ -229,10 +260,18 @@ export interface TestServicesBundle<
|
|
|
229
260
|
*/
|
|
230
261
|
readonly indexManager: StubIndexManager | undefined;
|
|
231
262
|
readonly modelService: ModelService<TAst, TDiagnostic, TTransfer>;
|
|
232
|
-
readonly transferEncoder: TransferEncoder<
|
|
263
|
+
readonly transferEncoder: TransferEncoder<TTransferDiagnostic>;
|
|
233
264
|
readonly logger: Logger;
|
|
234
265
|
/** The clock bound on the `Clock` slot — a `makeFakeClock()` if one was passed. */
|
|
235
266
|
readonly clock: Clock;
|
|
267
|
+
/**
|
|
268
|
+
* The service bound on `ServerLocale`, so a test can hand over a locale
|
|
269
|
+
* mid-run — which is also how it verifies the renderer reads the locale per
|
|
270
|
+
* render rather than caching it at construction.
|
|
271
|
+
*/
|
|
272
|
+
readonly serverLocale: ServerLocale;
|
|
273
|
+
/** The renderer bound on `MessageRenderer` — the framework's own unless one was supplied. */
|
|
274
|
+
readonly messageRenderer: MessageRenderer;
|
|
236
275
|
}
|
|
237
276
|
|
|
238
277
|
/**
|
|
@@ -247,12 +286,13 @@ export interface TestServicesBundle<
|
|
|
247
286
|
*/
|
|
248
287
|
export function makeTestServices<
|
|
249
288
|
TAst extends AstNode = AstNode,
|
|
250
|
-
TDiagnostic extends
|
|
289
|
+
TDiagnostic extends AstDiagnostic = AstDiagnostic,
|
|
251
290
|
TTransfer extends TransferElement = TransferElement,
|
|
252
|
-
TProject extends Project = Project
|
|
291
|
+
TProject extends Project = Project,
|
|
292
|
+
TTransferDiagnostic extends TransferDiagnostic = TransferDiagnostic
|
|
253
293
|
>(
|
|
254
|
-
options: MakeTestServicesOptions<TAst, TDiagnostic, TTransfer, TProject> = {}
|
|
255
|
-
): TestServicesBundle<TAst, TDiagnostic, TTransfer, TProject> {
|
|
294
|
+
options: MakeTestServicesOptions<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> = {}
|
|
295
|
+
): TestServicesBundle<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> {
|
|
256
296
|
const documents = makeStubLangiumDocuments<TAst, TDiagnostic>(options.seedDocuments);
|
|
257
297
|
const textDocuments = makeStubHydraniumTextDocuments();
|
|
258
298
|
const documentBuilder = makeStubDocumentBuilder();
|
|
@@ -289,7 +329,7 @@ export function makeTestServices<
|
|
|
289
329
|
// (after the factories run); the literal uses unsafe casts to
|
|
290
330
|
// partially-built objects to satisfy TestSharedServices, then patches
|
|
291
331
|
// them in.
|
|
292
|
-
const services: TestSharedServices<TAst, TDiagnostic, TTransfer, TProject> = {
|
|
332
|
+
const services: TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic> = {
|
|
293
333
|
Clock: clock,
|
|
294
334
|
Logger: logger,
|
|
295
335
|
Tracer: tracer,
|
|
@@ -311,20 +351,33 @@ export function makeTestServices<
|
|
|
311
351
|
...(indexManager ? { IndexManager: indexManager } : {}),
|
|
312
352
|
WorkspaceLock: new HydraniumWorkspaceLock()
|
|
313
353
|
},
|
|
314
|
-
model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject>['model']
|
|
354
|
+
model: {} as TestSharedServices<TAst, TDiagnostic, TTransfer, TProject, TTransferDiagnostic>['model'],
|
|
355
|
+
ServerLocale: {} as ServerLocale,
|
|
356
|
+
MessageRenderer: {} as DefaultMessageRenderer
|
|
315
357
|
};
|
|
316
358
|
const sharedServices = services as unknown as ServerSharedServices<TProject>;
|
|
317
359
|
|
|
360
|
+
// Patched in after the literal, like `model` below: both read the tree they
|
|
361
|
+
// belong to, and the renderer reads the locale service.
|
|
362
|
+
const mutableMessages = services as { ServerLocale: ServerLocale; MessageRenderer: MessageRenderer };
|
|
363
|
+
const serverLocale = new DefaultServerLocale(sharedServices);
|
|
364
|
+
if (options.locale) {
|
|
365
|
+
serverLocale.accept(options.locale);
|
|
366
|
+
}
|
|
367
|
+
mutableMessages.ServerLocale = serverLocale;
|
|
368
|
+
const messageRenderer = options.messageRenderer?.(sharedServices) ?? new DefaultMessageRenderer(sharedServices);
|
|
369
|
+
mutableMessages.MessageRenderer = messageRenderer;
|
|
370
|
+
|
|
318
371
|
const serialize = options.serialize ?? ((_uri: string, root: TTransfer) => JSON.stringify(root));
|
|
319
372
|
const transferEncoder = options.transferEncoder
|
|
320
373
|
? options.transferEncoder(sharedServices)
|
|
321
|
-
: new
|
|
374
|
+
: new DefaultTransferEncoder<unknown, TTransferDiagnostic>(sharedServices);
|
|
322
375
|
const modelService = options.modelService
|
|
323
376
|
? options.modelService(sharedServices)
|
|
324
377
|
: makeStubModelService<TAst, TDiagnostic, TTransfer>(sharedServices, serialize, options.modelServiceOptions);
|
|
325
378
|
|
|
326
379
|
const mutableModel = services.model as {
|
|
327
|
-
TransferEncoder: TransferEncoder<
|
|
380
|
+
TransferEncoder: TransferEncoder<TTransferDiagnostic>;
|
|
328
381
|
ModelService: ModelService<TAst, TDiagnostic, TTransfer>;
|
|
329
382
|
};
|
|
330
383
|
mutableModel.TransferEncoder = transferEncoder;
|
|
@@ -346,6 +399,8 @@ export function makeTestServices<
|
|
|
346
399
|
transferEncoder,
|
|
347
400
|
logger,
|
|
348
401
|
clock,
|
|
402
|
+
serverLocale,
|
|
403
|
+
messageRenderer,
|
|
349
404
|
dispose: () => {
|
|
350
405
|
// No-op: the bundled stubs hold only in-memory state (maps, arrays),
|
|
351
406
|
// released with the bundle when it goes out of scope. The hook exists
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
* committed sample workspace can **rewrite the sample files**, after which
|
|
22
22
|
* every later run asserts against already-repaired input: a suite that passes
|
|
23
23
|
* while testing nothing. The same applies to a fixture built in place, which
|
|
24
|
-
* additionally
|
|
25
|
-
* through
|
|
24
|
+
* additionally comes back in the serializer's layout, because the write-back
|
|
25
|
+
* goes through it.
|
|
26
26
|
*
|
|
27
27
|
* The second reason is cleanup. Hand-rolled `mkdtempSync` without a matching
|
|
28
28
|
* `rmSync` leaks a directory per test run into the OS temp dir, which nobody
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
51
|
import { URI } from '@hydranium/langium';
|
|
52
|
-
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
52
|
+
import { cpSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
53
53
|
import { tmpdir } from 'node:os';
|
|
54
54
|
import * as path from 'node:path';
|
|
55
55
|
|
|
@@ -106,7 +106,12 @@ export interface ScratchWorkspace {
|
|
|
106
106
|
* returned {@link ScratchWorkspace.dispose} with an `afterEach`.
|
|
107
107
|
*/
|
|
108
108
|
export function makeScratchWorkspace(options: ScratchWorkspaceOptions = {}): ScratchWorkspace {
|
|
109
|
-
|
|
109
|
+
// RESOLVED, because `tmpdir()` can itself be a symlink — on macOS it answers
|
|
110
|
+
// `/tmp`, which links to `/private/tmp`. A root the server canonicalises to a
|
|
111
|
+
// different path than the one it was handed registers documents under one
|
|
112
|
+
// spelling and reads them under the other, failing initialization with an
|
|
113
|
+
// ENOENT that names neither the link nor this call.
|
|
114
|
+
const root = realpathSync(mkdtempSync(path.join(tmpdir(), options.prefix ?? 'hydranium-scratch-')));
|
|
110
115
|
if (options.seed) {
|
|
111
116
|
cpSync(options.seed, root, { recursive: true });
|
|
112
117
|
}
|
|
@@ -203,6 +203,37 @@ export interface SpawnedServer extends Harness {
|
|
|
203
203
|
dispose(): Promise<number | null>;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Every server started and not yet disposed, so {@link disposeSpawnedServers}
|
|
208
|
+
* can reach one a test forgot.
|
|
209
|
+
*
|
|
210
|
+
* Holds the server — and through it the `ChildProcess` — rather than a pid. A
|
|
211
|
+
* pid identifies a process only while that process is alive: once it exits the
|
|
212
|
+
* OS may reissue the number, and Windows does so within seconds. A teardown
|
|
213
|
+
* that signalled a recorded pid would therefore reach whatever holds it now,
|
|
214
|
+
* which on a concurrent build is another task's compiler or test runner.
|
|
215
|
+
*/
|
|
216
|
+
const liveServers = new Set<SpawnedServer>();
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Terminate every server this module started that has not been disposed, and
|
|
220
|
+
* forget them.
|
|
221
|
+
*
|
|
222
|
+
* Safe to call when none are outstanding, and safe to call twice. Nothing here
|
|
223
|
+
* can reach a process this module did not spawn, because every kill goes
|
|
224
|
+
* through the `ChildProcess` handle libuv holds and libuv will not signal a
|
|
225
|
+
* child it has already reaped.
|
|
226
|
+
*
|
|
227
|
+
* A suite that disposes each server as it finishes needs this only as a net; a
|
|
228
|
+
* suite that does not needs it, because an orphan holding a port or a pipe
|
|
229
|
+
* makes the NEXT run's verdict meaningless.
|
|
230
|
+
*/
|
|
231
|
+
export async function disposeSpawnedServers(): Promise<void> {
|
|
232
|
+
const outstanding = [...liveServers];
|
|
233
|
+
liveServers.clear();
|
|
234
|
+
await Promise.all(outstanding.map(server => server.dispose()));
|
|
235
|
+
}
|
|
236
|
+
|
|
206
237
|
/** The captured streams, quoted into a boot-failure message. */
|
|
207
238
|
function bootDiagnosis(stdoutPrefix: string, stderr: string): string {
|
|
208
239
|
return (
|
|
@@ -390,7 +421,7 @@ export async function startSpawnedServer(options: SpawnedServerOptions): Promise
|
|
|
390
421
|
return exitCode;
|
|
391
422
|
}
|
|
392
423
|
|
|
393
|
-
|
|
424
|
+
const server: SpawnedServer = {
|
|
394
425
|
connection,
|
|
395
426
|
pid: child.pid,
|
|
396
427
|
initializeResult,
|
|
@@ -440,8 +471,11 @@ export async function startSpawnedServer(options: SpawnedServerOptions): Promise
|
|
|
440
471
|
});
|
|
441
472
|
},
|
|
442
473
|
dispose(): Promise<number | null> {
|
|
474
|
+
liveServers.delete(server);
|
|
443
475
|
disposal ??= terminate();
|
|
444
476
|
return disposal;
|
|
445
477
|
}
|
|
446
478
|
};
|
|
479
|
+
liveServers.add(server);
|
|
480
|
+
return server;
|
|
447
481
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { createServer as createHttpServer, type Server as HttpServer } from 'node:http';
|
|
11
|
+
import { connect, createServer as createTcpServer, type Server as TcpServer, type Socket } from 'node:net';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A TCP proxy that can half-close a connection, so an e2e suite can reproduce a
|
|
15
|
+
* flaky network against a real browser session.
|
|
16
|
+
*
|
|
17
|
+
* Toggling the browser offline is not enough. On localhost the server sees the
|
|
18
|
+
* close immediately and reaps the socket before the browser reconnects, so the
|
|
19
|
+
* window where both sides disagree about the session — the one where reconnect
|
|
20
|
+
* defects live — never opens. Reaching it needs the browser's socket to die
|
|
21
|
+
* while the server still believes it is alive, and nothing inside the app or in
|
|
22
|
+
* Playwright's offline emulation can produce that asymmetry: both close cleanly
|
|
23
|
+
* at both ends.
|
|
24
|
+
*
|
|
25
|
+
* The proxy holds both halves of every connection and lets a test kill them
|
|
26
|
+
* independently:
|
|
27
|
+
*
|
|
28
|
+
* `break` destroy the BROWSER-side sockets, keep the server-side ones open.
|
|
29
|
+
* The browser reconnects through a fresh pair while the server
|
|
30
|
+
* still holds the old one.
|
|
31
|
+
* `release` destroy the orphaned server-side sockets `break` left. The
|
|
32
|
+
* server's disconnect handler fires at once, rather than whenever
|
|
33
|
+
* its next heartbeat write would have failed.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** Where the proxy listens, and the application it fronts. */
|
|
37
|
+
export interface FlakyNetworkProxyOptions {
|
|
38
|
+
/** Port the browser connects to. */
|
|
39
|
+
readonly listenPort: number;
|
|
40
|
+
/** Port of the application being proxied. */
|
|
41
|
+
readonly targetPort: number;
|
|
42
|
+
/** Host of the application being proxied. Defaults to loopback. */
|
|
43
|
+
readonly targetHost?: string;
|
|
44
|
+
/** Port the HTTP control channel listens on. Defaults to `listenPort + 1000`. */
|
|
45
|
+
readonly controlPort?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The commands the control channel accepts, and the proxy's own vocabulary. */
|
|
49
|
+
export type FlakyNetworkCommand = 'break' | 'release' | 'status';
|
|
50
|
+
|
|
51
|
+
/** Drives a running proxy. Implemented both in-process and over HTTP. */
|
|
52
|
+
export interface FlakyNetworkControl {
|
|
53
|
+
/** Kill the browser side of every live connection; leave the server side up. */
|
|
54
|
+
break(): Promise<string>;
|
|
55
|
+
/** Kill the server side of every connection `break` orphaned. */
|
|
56
|
+
release(): Promise<string>;
|
|
57
|
+
/** How many connections are live and how many are orphaned. */
|
|
58
|
+
status(): Promise<string>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface FlakyNetworkProxy extends FlakyNetworkControl {
|
|
62
|
+
/** Base URL a browser should be pointed at, i.e. Playwright's `baseURL`. */
|
|
63
|
+
readonly url: string;
|
|
64
|
+
/** Control channel, for workers that cannot reach this object directly. */
|
|
65
|
+
readonly controlUrl: string;
|
|
66
|
+
close(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface ConnectionPair {
|
|
70
|
+
readonly id: number;
|
|
71
|
+
readonly browserSocket: Socket;
|
|
72
|
+
readonly targetSocket: Socket;
|
|
73
|
+
orphaned: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Starts the proxy in the calling process.
|
|
78
|
+
*
|
|
79
|
+
* Call it from a Playwright config, which runs in the main process before any
|
|
80
|
+
* worker forks. Workers are separate processes and cannot reach the returned
|
|
81
|
+
* object, which is what {@link controlUrl} and {@link flakyNetworkControlClient}
|
|
82
|
+
* are for.
|
|
83
|
+
*/
|
|
84
|
+
export async function startFlakyNetworkProxy(options: FlakyNetworkProxyOptions): Promise<FlakyNetworkProxy> {
|
|
85
|
+
const targetHost = options.targetHost ?? '127.0.0.1';
|
|
86
|
+
const controlPort = options.controlPort ?? options.listenPort + 1000;
|
|
87
|
+
const pairs = new Set<ConnectionPair>();
|
|
88
|
+
let sequence = 0;
|
|
89
|
+
let transcript: string[] = [];
|
|
90
|
+
|
|
91
|
+
const log = (message: string): void => {
|
|
92
|
+
transcript.push(message);
|
|
93
|
+
console.log(`[flaky-network] ${message}`);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const proxyServer: TcpServer = createTcpServer(browserSocket => {
|
|
97
|
+
const id = ++sequence;
|
|
98
|
+
const targetSocket = connect(options.targetPort, targetHost);
|
|
99
|
+
const pair: ConnectionPair = { id, browserSocket, targetSocket, orphaned: false };
|
|
100
|
+
pairs.add(pair);
|
|
101
|
+
|
|
102
|
+
// Forwarded by hand rather than with `pipe()`, so killing one side does not
|
|
103
|
+
// tear the other down — which is the entire point of the proxy.
|
|
104
|
+
browserSocket.on('data', chunk => {
|
|
105
|
+
if (!targetSocket.destroyed) {
|
|
106
|
+
targetSocket.write(chunk);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
targetSocket.on('data', chunk => {
|
|
110
|
+
if (!pair.orphaned && !browserSocket.destroyed) {
|
|
111
|
+
browserSocket.write(chunk);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const teardown = (origin: 'browser' | 'target'): void => {
|
|
116
|
+
if (pair.orphaned && origin === 'browser') {
|
|
117
|
+
// Expected: `break` killed this side deliberately. The server side
|
|
118
|
+
// stays up until `release`.
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
pairs.delete(pair);
|
|
122
|
+
browserSocket.destroy();
|
|
123
|
+
targetSocket.destroy();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
browserSocket.on('close', () => teardown('browser'));
|
|
127
|
+
targetSocket.on('close', () => teardown('target'));
|
|
128
|
+
// A half-killed pair reliably raises ECONNRESET on the surviving side; an
|
|
129
|
+
// unhandled 'error' would take the whole runner down with it.
|
|
130
|
+
browserSocket.on('error', () => undefined);
|
|
131
|
+
targetSocket.on('error', () => undefined);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const commands: Record<FlakyNetworkCommand, () => void> = {
|
|
135
|
+
break: () => {
|
|
136
|
+
const live = [...pairs].filter(pair => !pair.orphaned);
|
|
137
|
+
if (live.length === 0) {
|
|
138
|
+
log('nothing to break');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
for (const pair of live) {
|
|
142
|
+
pair.orphaned = true;
|
|
143
|
+
pair.browserSocket.destroy();
|
|
144
|
+
}
|
|
145
|
+
log(`${live.length} connection(s) broken; the browser should reconnect shortly`);
|
|
146
|
+
},
|
|
147
|
+
release: () => {
|
|
148
|
+
const orphans = [...pairs].filter(pair => pair.orphaned);
|
|
149
|
+
if (orphans.length === 0) {
|
|
150
|
+
log('no orphaned connections; run `break` first');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const pair of orphans) {
|
|
154
|
+
pairs.delete(pair);
|
|
155
|
+
pair.targetSocket.destroy();
|
|
156
|
+
}
|
|
157
|
+
log(`${orphans.length} orphaned connection(s) destroyed; their late disconnect fires now`);
|
|
158
|
+
},
|
|
159
|
+
status: () => {
|
|
160
|
+
const live = [...pairs].filter(pair => !pair.orphaned).length;
|
|
161
|
+
const orphaned = pairs.size - live;
|
|
162
|
+
log(`${live} live, ${orphaned} orphaned`);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const run = (name: string): string => {
|
|
167
|
+
transcript = [];
|
|
168
|
+
const command = commands[name as FlakyNetworkCommand];
|
|
169
|
+
if (command) {
|
|
170
|
+
command();
|
|
171
|
+
} else {
|
|
172
|
+
log(`unknown command '${name}'; use: ${Object.keys(commands).join(' | ')}`);
|
|
173
|
+
}
|
|
174
|
+
return transcript.join('\n');
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const controlServer: HttpServer = createHttpServer((request, response) => {
|
|
178
|
+
const output = run((request.url ?? '').replace(/^\//, '').split('?')[0]);
|
|
179
|
+
response.writeHead(200, { 'content-type': 'text/plain' });
|
|
180
|
+
response.end(`${output}\n`);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await Promise.all([listen(proxyServer, options.listenPort), listen(controlServer, controlPort)]);
|
|
184
|
+
// Neither server keeps the process alive, so a runner that starts one from a
|
|
185
|
+
// config does not need a teardown hook to exit — and a caller that wants a
|
|
186
|
+
// deterministic shutdown still has `close()`.
|
|
187
|
+
proxyServer.unref();
|
|
188
|
+
controlServer.unref();
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
url: `http://localhost:${options.listenPort}`,
|
|
192
|
+
controlUrl: `http://localhost:${controlPort}`,
|
|
193
|
+
break: async () => run('break'),
|
|
194
|
+
release: async () => run('release'),
|
|
195
|
+
status: async () => run('status'),
|
|
196
|
+
close: async () => {
|
|
197
|
+
for (const pair of pairs) {
|
|
198
|
+
pair.browserSocket.destroy();
|
|
199
|
+
pair.targetSocket.destroy();
|
|
200
|
+
}
|
|
201
|
+
pairs.clear();
|
|
202
|
+
await Promise.all([close(proxyServer), close(controlServer)]);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Drives a proxy running in another process, over its control channel.
|
|
209
|
+
*
|
|
210
|
+
* This is what a spec uses: Playwright workers are separate processes from the
|
|
211
|
+
* config that started the proxy.
|
|
212
|
+
*/
|
|
213
|
+
export function flakyNetworkControlClient(controlUrl: string): FlakyNetworkControl {
|
|
214
|
+
const send = async (command: FlakyNetworkCommand): Promise<string> => {
|
|
215
|
+
const response = await fetch(`${controlUrl}/${command}`);
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new Error(`flaky-network proxy rejected '${command}': ${response.status}`);
|
|
218
|
+
}
|
|
219
|
+
return (await response.text()).trim();
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
break: () => send('break'),
|
|
223
|
+
release: () => send('release'),
|
|
224
|
+
status: () => send('status')
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function listen(server: TcpServer | HttpServer, port: number): Promise<void> {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
server.once('error', reject);
|
|
231
|
+
server.listen(port, () => {
|
|
232
|
+
server.off('error', reject);
|
|
233
|
+
resolve();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function close(server: TcpServer | HttpServer): Promise<void> {
|
|
239
|
+
return new Promise(resolve => server.close(() => resolve()));
|
|
240
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// `@hydranium/core/testing` barrel) so importing the framework's unit-test
|
|
12
12
|
// helpers never pulls in `@playwright/test` — which is an OPTIONAL peer here.
|
|
13
13
|
export * from './e2e-profiling.js';
|
|
14
|
+
export * from './flaky-network-proxy.js';
|
|
14
15
|
export * from './browser-capture-bridge.js';
|
|
15
16
|
export * from './server-log-capture.js';
|
|
16
17
|
export * from './server-log-fixture.js';
|
|
@@ -160,16 +160,28 @@ export function serverLogSpecName(file: string): string {
|
|
|
160
160
|
* Rename each `<token>.log` (backend) and `<token>.browser.log` (forwarded
|
|
161
161
|
* browser console) to the spec name recorded in its `<token>.spec` sidecar
|
|
162
162
|
* (written by {@link markServerLog}), then delete the sidecar. Colliding names —
|
|
163
|
-
* a spec that opened more than one workspace — get a `-2`, `-3`, … suffix.
|
|
164
|
-
*
|
|
165
|
-
*
|
|
163
|
+
* a spec that opened more than one workspace — get a `-2`, `-3`, … suffix. Run
|
|
164
|
+
* once, after all server processes have exited (e.g. a reporter's `onEnd`).
|
|
165
|
+
* Best-effort.
|
|
166
|
+
*
|
|
167
|
+
* Returns the workspace tokens left UNATTRIBUTED — a log the capture wrote that
|
|
168
|
+
* no test claimed, because that spec never reached
|
|
169
|
+
* {@link markServerLog} (no `serverLog` fixture, no `beforeEach` call).
|
|
170
|
+
*
|
|
171
|
+
* **Returned rather than ignored, because the unattributed state is the
|
|
172
|
+
* misleading one.** Such a log still exists, still has content, and is named by
|
|
173
|
+
* an opaque token with no test boundaries in it — so a capture run that covered
|
|
174
|
+
* nothing looks exactly like one that covered everything, and the logs are
|
|
175
|
+
* there to be read. A caller that can warn should warn.
|
|
166
176
|
*/
|
|
167
|
-
export function renameServerLogs(dir: string):
|
|
177
|
+
export function renameServerLogs(dir: string): string[] {
|
|
168
178
|
let entries: string[];
|
|
169
179
|
try {
|
|
170
180
|
entries = readdirSync(dir);
|
|
171
181
|
} catch {
|
|
172
|
-
|
|
182
|
+
// An unreadable capture dir has nothing to report — including nothing
|
|
183
|
+
// unattributed, since no log was observed at all.
|
|
184
|
+
return [];
|
|
173
185
|
}
|
|
174
186
|
// Per suffix (`.log`, `.browser.log`) a `taken` set guards collisions, so the
|
|
175
187
|
// backend and browser logs for one spec share the same friendly base name.
|
|
@@ -190,6 +202,11 @@ export function renameServerLogs(dir: string): void {
|
|
|
190
202
|
// ignore
|
|
191
203
|
}
|
|
192
204
|
};
|
|
205
|
+
// Every token the capture produced a backend log for, so the ones no sidecar
|
|
206
|
+
// claims can be reported rather than silently left behind.
|
|
207
|
+
const unattributed = new Set(
|
|
208
|
+
entries.filter(entry => entry.endsWith('.log') && !entry.endsWith('.browser.log')).map(entry => entry.slice(0, -'.log'.length))
|
|
209
|
+
);
|
|
193
210
|
for (const entry of entries) {
|
|
194
211
|
if (!entry.endsWith('.spec')) {
|
|
195
212
|
continue;
|
|
@@ -210,9 +227,11 @@ export function renameServerLogs(dir: string): void {
|
|
|
210
227
|
if (!name) {
|
|
211
228
|
continue;
|
|
212
229
|
}
|
|
230
|
+
unattributed.delete(token);
|
|
213
231
|
rename(token, name, '.log');
|
|
214
232
|
rename(token, name, '.browser.log');
|
|
215
233
|
}
|
|
234
|
+
return [...unattributed];
|
|
216
235
|
}
|
|
217
236
|
|
|
218
237
|
/**
|