tungsten 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1246 -0
- package/dist/.build-complete +1 -1
- package/dist/routing/Routes.d.ts +3 -1
- package/package.json +6 -3
- package/dist/AnyComponent.d.ts.map +0 -1
- package/dist/AnyComponentType.d.ts.map +0 -1
- package/dist/AppResponse.d.ts.map +0 -1
- package/dist/AppTypeExtraction.d.ts.map +0 -1
- package/dist/AssignedStyle.d.ts.map +0 -1
- package/dist/Brands.d.ts.map +0 -1
- package/dist/CjsTypes.d.ts.map +0 -1
- package/dist/ComponentInstance.d.ts.map +0 -1
- package/dist/ComponentRenderInfo.type.d.ts.map +0 -1
- package/dist/ComponentStyle.d.ts.map +0 -1
- package/dist/Components.d.ts.map +0 -1
- package/dist/ContentFunction.d.ts.map +0 -1
- package/dist/ContentNode.d.ts.map +0 -1
- package/dist/ContentNodeNew.d.ts.map +0 -1
- package/dist/CssNames.d.ts.map +0 -1
- package/dist/DomNodes.d.ts.map +0 -1
- package/dist/DomPointer.d.ts.map +0 -1
- package/dist/DomRegion.d.ts.map +0 -1
- package/dist/ExtendableHTMLElement.d.ts.map +0 -1
- package/dist/Framework.d.ts.map +0 -1
- package/dist/FrameworkConfig.d.ts.map +0 -1
- package/dist/FrameworkConfig.types.test.d.ts +0 -2
- package/dist/FrameworkConfig.types.test.d.ts.map +0 -1
- package/dist/FrameworkConfig.types.test.js +0 -6
- package/dist/FunctionComponent.d.ts.map +0 -1
- package/dist/FunctionComponentStandIn.d.ts.map +0 -1
- package/dist/GenericObject.type.d.ts.map +0 -1
- package/dist/HtmlElementAttributes.d.ts.map +0 -1
- package/dist/Instance/ClientRuntimeContext.d.ts.map +0 -1
- package/dist/Instance/Instance.d.ts.map +0 -1
- package/dist/Instance/InstanceConfig.d.ts.map +0 -1
- package/dist/IsDefined.d.ts.map +0 -1
- package/dist/IsIndexableObject.d.ts.map +0 -1
- package/dist/IsNumber.d.ts.map +0 -1
- package/dist/IsObject.d.ts.map +0 -1
- package/dist/Json.type.d.ts.map +0 -1
- package/dist/JsxContent.d.ts.map +0 -1
- package/dist/Key.d.ts.map +0 -1
- package/dist/KeyAttr.d.ts.map +0 -1
- package/dist/MapObject.d.ts.map +0 -1
- package/dist/Mode.d.ts.map +0 -1
- package/dist/Module.d.ts.map +0 -1
- package/dist/NoDeproxy.d.ts.map +0 -1
- package/dist/PopMessage.type.d.ts.map +0 -1
- package/dist/ProcessedStylesObject.d.ts.map +0 -1
- package/dist/RenderFunction.d.ts.map +0 -1
- package/dist/RenderedContentNode.d.ts.map +0 -1
- package/dist/RenderingNode.d.ts.map +0 -1
- package/dist/ReqRes.d.ts.map +0 -1
- package/dist/Sass.d.ts.map +0 -1
- package/dist/Script.d.ts.map +0 -1
- package/dist/ServerRpcFunction.type.d.ts.map +0 -1
- package/dist/Servers/DevServer.d.ts.map +0 -1
- package/dist/Servers/ProdServer.d.ts.map +0 -1
- package/dist/Servers/ServerMiddleware.d.ts.map +0 -1
- package/dist/Servers/StaticSiteServer.d.ts.map +0 -1
- package/dist/Servers/TungstenServer.d.ts.map +0 -1
- package/dist/Servers/cacheVersioned.d.ts.map +0 -1
- package/dist/Servers/devErrorPageAutoRetry.d.ts.map +0 -1
- package/dist/Servers/initStores.d.ts.map +0 -1
- package/dist/Servers/listenVerified.d.ts.map +0 -1
- package/dist/Servers/loadEnvsDev.d.ts.map +0 -1
- package/dist/Servers/portError.d.ts.map +0 -1
- package/dist/Servers/resetRegistrations.d.ts.map +0 -1
- package/dist/Servers/routing.d.ts.map +0 -1
- package/dist/Servers/serveStatic.d.ts.map +0 -1
- package/dist/Servers/stubInstance.d.ts.map +0 -1
- package/dist/Servers/tungstenCorePrivateEnvSchema.d.ts.map +0 -1
- package/dist/Servers/tungstenCorePublicEnvSchema.d.ts.map +0 -1
- package/dist/Store/BaseStoreSchema.d.ts.map +0 -1
- package/dist/Store/InternalStoreSchema.d.ts.map +0 -1
- package/dist/Store/ServerDataLoading.d.ts.map +0 -1
- package/dist/Store/Store.d.ts.map +0 -1
- package/dist/Store/StoreProxyInterface.d.ts.map +0 -1
- package/dist/Store/StoreSchemaClass.d.ts.map +0 -1
- package/dist/Store/StoreSubscriptions.types.d.ts.map +0 -1
- package/dist/Store/TypeSafeStore.d.ts.map +0 -1
- package/dist/Store/createStores.d.ts.map +0 -1
- package/dist/Store/exportStore.d.ts.map +0 -1
- package/dist/Store/getInternalStore.d.ts.map +0 -1
- package/dist/Store/getStore.d.ts.map +0 -1
- package/dist/Store/makeStore.d.ts.map +0 -1
- package/dist/Store/registerStore.d.ts.map +0 -1
- package/dist/Store/storeCreators.d.ts.map +0 -1
- package/dist/StoreBindVariable.d.ts.map +0 -1
- package/dist/StyleProcessor.d.ts.map +0 -1
- package/dist/TElement.d.ts.map +0 -1
- package/dist/TungstenApp.d.ts.map +0 -1
- package/dist/TungstenAppOld.d.ts.map +0 -1
- package/dist/TungstenElement.d.ts.map +0 -1
- package/dist/TungstenError.d.ts.map +0 -1
- package/dist/TungstenStyleObject.d.ts.map +0 -1
- package/dist/__deproxy.d.ts.map +0 -1
- package/dist/_client/RootComponent.d.ts.map +0 -1
- package/dist/_client/cjsHelpers.d.ts.map +0 -1
- package/dist/_client/client.d.ts.map +0 -1
- package/dist/_client/hmr.d.ts.map +0 -1
- package/dist/_client/liveReload.d.ts.map +0 -1
- package/dist/_client/liveReloadSource.d.ts.map +0 -1
- package/dist/_client/rootComponentRelFilePath.d.ts.map +0 -1
- package/dist/_client/routes.d.ts.map +0 -1
- package/dist/_testing/regexEquals.d.ts.map +0 -1
- package/dist/_testing/should.d.ts.map +0 -1
- package/dist/_testing/sxy-test-runner.types.d.ts.map +0 -1
- package/dist/alive.d.ts.map +0 -1
- package/dist/api/ApiEndpoint.d.ts.map +0 -1
- package/dist/api/ApiMethod.type.d.ts.map +0 -1
- package/dist/api/ApiRoute.d.ts.map +0 -1
- package/dist/api/ApiRoutes.d.ts.map +0 -1
- package/dist/api/Request.d.ts.map +0 -1
- package/dist/api/Response.d.ts.map +0 -1
- package/dist/api/getApiRoutes.d.ts.map +0 -1
- package/dist/api/getExport.d.ts.map +0 -1
- package/dist/api/getExportDev.d.ts.map +0 -1
- package/dist/appBuilder.d.ts.map +0 -1
- package/dist/appBuilder.types.test.d.ts +0 -110
- package/dist/appBuilder.types.test.d.ts.map +0 -1
- package/dist/appBuilder.types.test.js +0 -240
- package/dist/appOutput.d.ts.map +0 -1
- package/dist/assert.d.ts.map +0 -1
- package/dist/auth/checkPassword.d.ts.map +0 -1
- package/dist/auth/hashPassword.d.ts.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/babelPlugins/BabelAddDevCacheBust.d.ts.map +0 -1
- package/dist/babelPlugins/BabelExtractComponentStylesheet.d.ts.map +0 -1
- package/dist/babelPlugins/BabelFakeType.d.ts.map +0 -1
- package/dist/babelPlugins/BabelState.type.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformBindDirect.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformBindVar.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformCjsToEsm.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformCjsToFunction.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformCjsToFunction2.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformComponentState.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformComponentStyles.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformCssImports.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformDependencyImportsOld.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformDeproxyState.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformDocInit.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformJsxExtensionsToJs.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformJsxSignals.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformPackageImports.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformReactive.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformReactiveSvelteStyle.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformRemoveBlocks.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformRemoveDevBlocks.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformRemoveServerBlocks.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformRemoveServerCode.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformSxyFrameworkImports.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformSxyJsx.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTransformUniqueComponents.d.ts.map +0 -1
- package/dist/babelPlugins/BabelTypes.d.ts.map +0 -1
- package/dist/babelPlugins/fixImportsInSassErrorMessage.d.ts.map +0 -1
- package/dist/babelPlugins/isBareImport.d.ts.map +0 -1
- package/dist/babelPlugins/test.d.ts.map +0 -1
- package/dist/babelPlugins/transformPackageImports/BabelTransformDependencyImportsCjs.d.ts.map +0 -1
- package/dist/babelPlugins/transformPackageImports/BabelTransformDependencyImportsEsm.d.ts.map +0 -1
- package/dist/babelPlugins/transformPackageImports/getWebLocationOfPackageFile.d.ts.map +0 -1
- package/dist/babelPlugins/transformPackageImports/resolveBareImport.d.ts.map +0 -1
- package/dist/bin/args.d.ts.map +0 -1
- package/dist/bin/commands/build.d.ts.map +0 -1
- package/dist/bin/commands/buildHelp.d.ts.map +0 -1
- package/dist/bin/commands/clean.d.ts.map +0 -1
- package/dist/bin/commands/compile.d.ts.map +0 -1
- package/dist/bin/commands/compileSimplePackage.d.ts.map +0 -1
- package/dist/bin/commands/dev.d.ts.map +0 -1
- package/dist/bin/commands/devmon.d.ts.map +0 -1
- package/dist/bin/commands/help.d.ts.map +0 -1
- package/dist/bin/commands/speed.d.ts.map +0 -1
- package/dist/bin/commands/start.d.ts.map +0 -1
- package/dist/bin/commands/static.d.ts.map +0 -1
- package/dist/bin/index.d.ts.map +0 -1
- package/dist/bin/moduleLoadAnalysis.d.ts.map +0 -1
- package/dist/bin/vendorHook.d.ts.map +0 -1
- package/dist/booleanAttributeValue.d.ts.map +0 -1
- package/dist/browserStorage.d.ts.map +0 -1
- package/dist/build/BabelConfig.d.ts.map +0 -1
- package/dist/build/CompilationInfo.d.ts.map +0 -1
- package/dist/build/CompilationInfoFailure.d.ts.map +0 -1
- package/dist/build/CompilationInfoSuccess.d.ts.map +0 -1
- package/dist/build/Event.type.d.ts.map +0 -1
- package/dist/build/ImportAliases.d.ts.map +0 -1
- package/dist/build/PackageList.d.ts.map +0 -1
- package/dist/build/PackagesCacheInfo.d.ts.map +0 -1
- package/dist/build/ProcessPackageResult.d.ts.map +0 -1
- package/dist/build/ProcessPackageResults.d.ts.map +0 -1
- package/dist/build/PromiseSettledResultIsPromiseRejectedResult.d.ts.map +0 -1
- package/dist/build/assetVersions.d.ts.map +0 -1
- package/dist/build/babel.user.common.config.d.ts.map +0 -1
- package/dist/build/bundle.d.ts.map +0 -1
- package/dist/build/bundleBabelOptions.d.ts.map +0 -1
- package/dist/build/clientModuleAliases.d.ts.map +0 -1
- package/dist/build/clientRoutesFile.d.ts.map +0 -1
- package/dist/build/combineComponentCss.d.ts.map +0 -1
- package/dist/build/compileJavascript.d.ts.map +0 -1
- package/dist/build/compileStyles.d.ts.map +0 -1
- package/dist/build/compileUserApp.d.ts.map +0 -1
- package/dist/build/componentStylesCache.d.ts.map +0 -1
- package/dist/build/copyAssets.d.ts.map +0 -1
- package/dist/build/copyPackageModulesToPackagesFolder.d.ts.map +0 -1
- package/dist/build/copyPublicFiles.d.ts.map +0 -1
- package/dist/build/createClientFiles.d.ts.map +0 -1
- package/dist/build/createInstanceConfigFile.d.ts.map +0 -1
- package/dist/build/createPackageVirtualFileSystem.d.ts.map +0 -1
- package/dist/build/createPackageVirtualFileSystems.d.ts.map +0 -1
- package/dist/build/customImportAliases.d.ts.map +0 -1
- package/dist/build/customPackageEntryPoints.d.ts.map +0 -1
- package/dist/build/devBundle.d.ts.map +0 -1
- package/dist/build/devBundle.rollup.config.d.ts.map +0 -1
- package/dist/build/findRequiredUserModules.d.ts.map +0 -1
- package/dist/build/frameworkDestLocation.d.ts.map +0 -1
- package/dist/build/frameworkStylesheets.d.ts.map +0 -1
- package/dist/build/getAllSxyClientModules.d.ts.map +0 -1
- package/dist/build/getPackageDirForFile.d.ts.map +0 -1
- package/dist/build/getSxyClientPackageAliases.d.ts.map +0 -1
- package/dist/build/getTempDirForAppCompilation.d.ts.map +0 -1
- package/dist/build/internalAliases.d.ts.map +0 -1
- package/dist/build/makeBabelRawConfig.d.ts.map +0 -1
- package/dist/build/makeBabelUserDevConfig.d.ts.map +0 -1
- package/dist/build/makeBabelUserProdConfig.d.ts.map +0 -1
- package/dist/build/makeEnvJavascriptContent.d.ts.map +0 -1
- package/dist/build/modifySassImports.d.ts.map +0 -1
- package/dist/build/packageIdentifier.d.ts.map +0 -1
- package/dist/build/processDataToLines.d.ts.map +0 -1
- package/dist/build/resolveSassScssImports.d.ts.map +0 -1
- package/dist/build/rollup.user.prod.config.d.ts.map +0 -1
- package/dist/build/rollupExtractComponentStylesheets.d.ts.map +0 -1
- package/dist/build/rollupProdConstants.d.ts.map +0 -1
- package/dist/build/sassImportParser.d.ts.map +0 -1
- package/dist/build/scssImportParserChatgpt.d.ts.map +0 -1
- package/dist/build/tallyBabel.d.ts.map +0 -1
- package/dist/cacheControl.d.ts.map +0 -1
- package/dist/calculateImportedStaticFileLocations.d.ts.map +0 -1
- package/dist/calledFrom.d.ts.map +0 -1
- package/dist/checkedAttributeToBoolean.d.ts.map +0 -1
- package/dist/checkedValueToBoolean.d.ts.map +0 -1
- package/dist/cliOutput.d.ts.map +0 -1
- package/dist/clone.d.ts.map +0 -1
- package/dist/compat/context.d.ts.map +0 -1
- package/dist/compat/forwardRef.d.ts.map +0 -1
- package/dist/compat/index.d.ts.map +0 -1
- package/dist/compat/propTypes.d.ts.map +0 -1
- package/dist/compat/toTungstenClassComponent.d.ts.map +0 -1
- package/dist/compat/toTungstenFunctionComponent.d.ts.map +0 -1
- package/dist/compat/types.d.ts.map +0 -1
- package/dist/componentClassName.d.ts.map +0 -1
- package/dist/componentHelpers.d.ts.map +0 -1
- package/dist/componentProxyHandlers.d.ts.map +0 -1
- package/dist/componentType.d.ts.map +0 -1
- package/dist/components/Await.d.ts.map +0 -1
- package/dist/components/Fragment.d.ts.map +0 -1
- package/dist/components/GoToComponent.d.ts.map +0 -1
- package/dist/components/Lazy.d.ts.map +0 -1
- package/dist/components/Link.d.ts.map +0 -1
- package/dist/components/NativeHtml.d.ts.map +0 -1
- package/dist/components/NoEscape.d.ts.map +0 -1
- package/dist/components/PopMessages.d.ts.map +0 -1
- package/dist/components/Protected.d.ts.map +0 -1
- package/dist/components/RawHtml.d.ts.map +0 -1
- package/dist/components/RouteRedirectComponent.d.ts.map +0 -1
- package/dist/components/Stylesheet.d.ts.map +0 -1
- package/dist/components/WebRedirectComponent.d.ts.map +0 -1
- package/dist/components/_GoToComponent.d.ts.map +0 -1
- package/dist/components/bindable/BindMode.d.ts.map +0 -1
- package/dist/components/bindable/BindVariable.d.ts.map +0 -1
- package/dist/components/bindable/BindableCheckbox.d.ts.map +0 -1
- package/dist/components/bindable/BindableInput.d.ts.map +0 -1
- package/dist/components/bindable/BindableInputNonString.d.ts.map +0 -1
- package/dist/components/bindable/BindableInputString.d.ts.map +0 -1
- package/dist/components/bindable/BindableInputTyped.d.ts.map +0 -1
- package/dist/components/bindable/BindableInputTypedLegacy.d.ts.map +0 -1
- package/dist/components/bindable/BindableRadio.d.ts.map +0 -1
- package/dist/components/bindable/BindableSelect.d.ts.map +0 -1
- package/dist/components/bindable/BindableSelectNaive.d.ts.map +0 -1
- package/dist/components/bindable/BindableTextarea.d.ts.map +0 -1
- package/dist/components/bindable/bindableCandidates.comparison.d.ts.map +0 -1
- package/dist/components/bindable/bindableHtmlElements.d.ts.map +0 -1
- package/dist/components/bindable/bindableProps.types.test.d.ts +0 -50
- package/dist/components/bindable/bindableProps.types.test.d.ts.map +0 -1
- package/dist/components/bindable/bindableProps.types.test.js +0 -252
- package/dist/components/library/RichTextEditor/RichTextEditor.d.ts.map +0 -1
- package/dist/components/library/RichTextEditor/StoredContentNode.d.ts.map +0 -1
- package/dist/components/library/RichTextEditor/activeFormats.d.ts.map +0 -1
- package/dist/components/library/RichTextEditor/contentTree.d.ts.map +0 -1
- package/dist/components/library/RichTextEditor/sanitizeAuthoredHtml.d.ts.map +0 -1
- package/dist/components/library/__StatefulMessage/MessageData.d.ts.map +0 -1
- package/dist/components/library/__StatefulMessage/StatefulMessage.d.ts.map +0 -1
- package/dist/components/library/index.d.ts.map +0 -1
- package/dist/components/makeTypedLink.d.ts.map +0 -1
- package/dist/concat.d.ts.map +0 -1
- package/dist/contentNodeSymbols.d.ts.map +0 -1
- package/dist/context/registerContext.d.ts.map +0 -1
- package/dist/copy.d.ts.map +0 -1
- package/dist/createElement.d.ts.map +0 -1
- package/dist/createRef.d.ts.map +0 -1
- package/dist/createRoutes.d.ts.map +0 -1
- package/dist/defaultConfig.d.ts.map +0 -1
- package/dist/deleteDirs.d.ts.map +0 -1
- package/dist/devBanner.d.ts.map +0 -1
- package/dist/docInit.d.ts.map +0 -1
- package/dist/domNodeChecks.d.ts.map +0 -1
- package/dist/env/Env.d.ts.map +0 -1
- package/dist/env/Interfaces.d.ts.map +0 -1
- package/dist/env/addToEnv.d.ts.map +0 -1
- package/dist/env/droppedCoreEnvKeys.d.ts.map +0 -1
- package/dist/env/funcs.d.ts.map +0 -1
- package/dist/env/getEnvSchemas.d.ts.map +0 -1
- package/dist/env/index.d.ts.map +0 -1
- package/dist/env/privateEnv.d.ts.map +0 -1
- package/dist/env/privateEnvOrThrow.d.ts.map +0 -1
- package/dist/env/publicEnv.d.ts.map +0 -1
- package/dist/env/publicEnvOrThrow.d.ts.map +0 -1
- package/dist/env/registerEnvSchemas.d.ts.map +0 -1
- package/dist/env/setPrivateEnvValidator.d.ts.map +0 -1
- package/dist/env/setPublicEnvValidator.d.ts.map +0 -1
- package/dist/env/test.d.ts.map +0 -1
- package/dist/env/types.d.ts.map +0 -1
- package/dist/env/windowGlobalEnvStatement.d.ts.map +0 -1
- package/dist/experiments.d.ts.map +0 -1
- package/dist/filePathFromUrl.d.ts.map +0 -1
- package/dist/fileVersions.d.ts.map +0 -1
- package/dist/findPackageDotJson.d.ts.map +0 -1
- package/dist/findProjectDir.d.ts.map +0 -1
- package/dist/findUserConfig.d.ts.map +0 -1
- package/dist/fixExtension.d.ts.map +0 -1
- package/dist/foreach.d.ts.map +0 -1
- package/dist/frameworkConfigSchema.d.ts.map +0 -1
- package/dist/getArrowFunctionBody.d.ts.map +0 -1
- package/dist/getComponent.d.ts.map +0 -1
- package/dist/getComponentDev.d.ts.map +0 -1
- package/dist/getConfig.d.ts.map +0 -1
- package/dist/getContentFromFunctionComponent.d.ts.map +0 -1
- package/dist/getFrameworkDir.d.ts.map +0 -1
- package/dist/getRoutes.d.ts.map +0 -1
- package/dist/getRoutesDevs.d.ts.map +0 -1
- package/dist/getStackTrace.d.ts.map +0 -1
- package/dist/getStyledContentFromFunctionComponent.d.ts.map +0 -1
- package/dist/getUserConfig.d.ts.map +0 -1
- package/dist/glob.d.ts.map +0 -1
- package/dist/globals.d.ts.map +0 -1
- package/dist/goto.d.ts.map +0 -1
- package/dist/hasDomain.d.ts.map +0 -1
- package/dist/hasValidKey.d.ts.map +0 -1
- package/dist/headChildren.d.ts.map +0 -1
- package/dist/helpers/combineClassNames.d.ts.map +0 -1
- package/dist/helpers/forwardingFetch.d.ts.map +0 -1
- package/dist/helpers/getFetchCompatibleHeaders.d.ts.map +0 -1
- package/dist/helpers/index.d.ts.map +0 -1
- package/dist/helpers/perRequest.d.ts.map +0 -1
- package/dist/hmr/applyHmrModule.d.ts.map +0 -1
- package/dist/hmr/componentIdentity.d.ts.map +0 -1
- package/dist/hmr/instanceRegistry.d.ts.map +0 -1
- package/dist/hmr/liveReloadHub.d.ts.map +0 -1
- package/dist/hmr/refreshStylesheets.d.ts.map +0 -1
- package/dist/hooks/clearCurrentComponent.d.ts.map +0 -1
- package/dist/hooks/getCurrentComponent.d.ts.map +0 -1
- package/dist/hooks/globalObj.d.ts.map +0 -1
- package/dist/hooks/globalType.d.ts.map +0 -1
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/setCurrentComponent.d.ts.map +0 -1
- package/dist/hooks/shallowCompare.d.ts.map +0 -1
- package/dist/hooks/useCallback.d.ts.map +0 -1
- package/dist/hooks/useContext.d.ts.map +0 -1
- package/dist/hooks/useEffect.d.ts.map +0 -1
- package/dist/hooks/useInstance.d.ts.map +0 -1
- package/dist/hooks/useMemo.d.ts.map +0 -1
- package/dist/hooks/useReducer.d.ts.map +0 -1
- package/dist/hooks/useRef.d.ts.map +0 -1
- package/dist/hooks/useState.d.ts.map +0 -1
- package/dist/htmlEntities.d.ts.map +0 -1
- package/dist/htmlEventHandlerKey.d.ts.map +0 -1
- package/dist/htmlEvents.d.ts.map +0 -1
- package/dist/hydrate/hydrate.d.ts.map +0 -1
- package/dist/hydrate/hydrateGlobalData.d.ts.map +0 -1
- package/dist/hydrate/reconcileHydratedAttributes.d.ts.map +0 -1
- package/dist/hydrate/renderHydrationMismatchBanner.d.ts.map +0 -1
- package/dist/hydrate/stubRouting.d.ts.map +0 -1
- package/dist/importDevModuleWithRetry.d.ts.map +0 -1
- package/dist/importModule.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/indexInner.d.ts.map +0 -1
- package/dist/info.d.ts.map +0 -1
- package/dist/innerHtmlFromProps.d.ts.map +0 -1
- package/dist/isBrowser.d.ts.map +0 -1
- package/dist/isClient.d.ts.map +0 -1
- package/dist/isDev.d.ts.map +0 -1
- package/dist/isProd.d.ts.map +0 -1
- package/dist/isServer.d.ts.map +0 -1
- package/dist/iterationQueryString.d.ts.map +0 -1
- package/dist/json.d.ts.map +0 -1
- package/dist/jsx-runtime.d.ts.map +0 -1
- package/dist/jsxHelpers.d.ts.map +0 -1
- package/dist/jsxRuntime.d.ts.map +0 -1
- package/dist/jsxRuntimeSignals.d.ts.map +0 -1
- package/dist/loadPackageDotJsonFromDir.d.ts.map +0 -1
- package/dist/logAndReturn.d.ts.map +0 -1
- package/dist/makeFinalConfig.d.ts.map +0 -1
- package/dist/makeInstanceConfigFromFrameworkConfig.d.ts.map +0 -1
- package/dist/makeServerRpc.d.ts.map +0 -1
- package/dist/markOptionSelected.d.ts.map +0 -1
- package/dist/mergeConfigs.d.ts.map +0 -1
- package/dist/moduleReloader.d.ts.map +0 -1
- package/dist/mutatingFunctions.d.ts.map +0 -1
- package/dist/nbsp.d.ts.map +0 -1
- package/dist/objectMap.d.ts.map +0 -1
- package/dist/objectTypesToCopy.d.ts.map +0 -1
- package/dist/objectTypesToProxy.d.ts.map +0 -1
- package/dist/output.d.ts.map +0 -1
- package/dist/packageSettings.d.ts.map +0 -1
- package/dist/pipeLog.d.ts.map +0 -1
- package/dist/pipeline/PipelineRequestHandler.d.ts.map +0 -1
- package/dist/pipeline/contextStore.d.ts.map +0 -1
- package/dist/pipeline/hydratePipeline.d.ts.map +0 -1
- package/dist/pipeline/importRoutesModule.d.ts.map +0 -1
- package/dist/pipeline/networkedTransport.d.ts.map +0 -1
- package/dist/pipeline/pipeline.d.ts.map +0 -1
- package/dist/pipeline/pipeline.types.test.d.ts +0 -760
- package/dist/pipeline/pipeline.types.test.d.ts.map +0 -1
- package/dist/pipeline/pipeline.types.test.js +0 -344
- package/dist/pipeline/pipelineDefinition.d.ts.map +0 -1
- package/dist/pipeline/pipelineSplit.types.test.d.ts +0 -2445
- package/dist/pipeline/pipelineSplit.types.test.d.ts.map +0 -1
- package/dist/pipeline/pipelineSplit.types.test.js +0 -440
- package/dist/pipeline/runPipeline.d.ts.map +0 -1
- package/dist/pipeline/runSplitPipeline.d.ts.map +0 -1
- package/dist/pipeline/splitPipeline.d.ts.map +0 -1
- package/dist/pop.d.ts.map +0 -1
- package/dist/possibleEnvFiles.d.ts.map +0 -1
- package/dist/postcssPlugins/TransformCssNames.d.ts.map +0 -1
- package/dist/postcssPlugins/parseSelectors.d.ts.map +0 -1
- package/dist/postcssPlugins/parseSelectorsRich.d.ts.map +0 -1
- package/dist/postcssPlugins/scopeAnimationValue.d.ts.map +0 -1
- package/dist/postcssPlugins/splitCssValue.d.ts.map +0 -1
- package/dist/postcssPlugins/unwrapIgnoreMarker.d.ts.map +0 -1
- package/dist/promise.d.ts.map +0 -1
- package/dist/propsAreEqual.d.ts.map +0 -1
- package/dist/rawHtml/HtmlAllowList.d.ts.map +0 -1
- package/dist/rawHtml/decodeHtmlEntities.d.ts.map +0 -1
- package/dist/rawHtml/filterContentNode.d.ts.map +0 -1
- package/dist/rawHtml/htmlParsingTags.d.ts.map +0 -1
- package/dist/rawHtml/parseHtml.d.ts.map +0 -1
- package/dist/rawHtml/safeHtmlAllowList.d.ts.map +0 -1
- package/dist/rawHtml/sanitizeHtml.d.ts.map +0 -1
- package/dist/rawHtml/tokenizeHtml.d.ts.map +0 -1
- package/dist/reRouting/addRequireResolveHooks.d.ts.map +0 -1
- package/dist/reRouting/importLoader.d.ts.map +0 -1
- package/dist/ref.d.ts.map +0 -1
- package/dist/regexEscape.d.ts.map +0 -1
- package/dist/removeRecursive.d.ts.map +0 -1
- package/dist/render/HeadData.d.ts.map +0 -1
- package/dist/render/SxyFrameworkModule.d.ts.map +0 -1
- package/dist/render/SxyFrameworkModules.d.ts.map +0 -1
- package/dist/render/addImportMap.d.ts.map +0 -1
- package/dist/render/classComponentNodeToRenderingTree.d.ts.map +0 -1
- package/dist/render/componentNodeToRenderingTree.d.ts.map +0 -1
- package/dist/render/contentNodeToHtml.d.ts.map +0 -1
- package/dist/render/contentToHtml.d.ts.map +0 -1
- package/dist/render/contentToHtmlDev.d.ts.map +0 -1
- package/dist/render/contentTreeToRenderingTree.d.ts.map +0 -1
- package/dist/render/debug.d.ts.map +0 -1
- package/dist/render/functionComponentNodeToRenderingTree.d.ts.map +0 -1
- package/dist/render/getFunctionBodyAsString.d.ts.map +0 -1
- package/dist/render/htmlMonoTags.d.ts.map +0 -1
- package/dist/render/htmlNoSelfCloseTags.d.ts.map +0 -1
- package/dist/render/htmlSelfClosableTags.d.ts.map +0 -1
- package/dist/render/htmlSingleOnlyTags.d.ts.map +0 -1
- package/dist/render/htmlVoidTags.d.ts.map +0 -1
- package/dist/render/instantiateClassComponent.d.ts.map +0 -1
- package/dist/render/instantiateComponent.d.ts.map +0 -1
- package/dist/render/instantiateFunctionComponent.d.ts.map +0 -1
- package/dist/render/printComponentNameInSource.d.ts.map +0 -1
- package/dist/render/processBody.d.ts.map +0 -1
- package/dist/render/processHead.d.ts.map +0 -1
- package/dist/render/processHtmlNode.d.ts.map +0 -1
- package/dist/render/processProps.d.ts.map +0 -1
- package/dist/render/processRenderingNodeHtml.d.ts.map +0 -1
- package/dist/render/readDocInitScript.d.ts.map +0 -1
- package/dist/render/renderingTreeToHtml.d.ts.map +0 -1
- package/dist/render/rootToHtml.d.ts.map +0 -1
- package/dist/render/rootToHtmlDev.d.ts.map +0 -1
- package/dist/requests/axios/get.d.ts.map +0 -1
- package/dist/requests/axios/post.d.ts.map +0 -1
- package/dist/requests/fetch/FetchExtras.d.ts.map +0 -1
- package/dist/requests/fetch/_fetchPromise.d.ts.map +0 -1
- package/dist/requests/fetch/delete.d.ts.map +0 -1
- package/dist/requests/fetch/get.d.ts.map +0 -1
- package/dist/requests/fetch/mergeFetchInit.d.ts.map +0 -1
- package/dist/requests/fetch/paramsObjectToParamsString.d.ts.map +0 -1
- package/dist/requests/fetch/patch.d.ts.map +0 -1
- package/dist/requests/fetch/post.d.ts.map +0 -1
- package/dist/requests/fetch/put.d.ts.map +0 -1
- package/dist/requests/index.d.ts.map +0 -1
- package/dist/routesLocationParts.d.ts.map +0 -1
- package/dist/routing/AppRoutes.d.ts.map +0 -1
- package/dist/routing/Default404.d.ts.map +0 -1
- package/dist/routing/ErrorRouteType.d.ts.map +0 -1
- package/dist/routing/Group.d.ts.map +0 -1
- package/dist/routing/Match.type.d.ts.map +0 -1
- package/dist/routing/MatchedRoute.d.ts.map +0 -1
- package/dist/routing/MatchedRouteInfo.d.ts.map +0 -1
- package/dist/routing/Params.d.ts.map +0 -1
- package/dist/routing/ParamsFromPath.d.ts.map +0 -1
- package/dist/routing/RedirectCode.d.ts.map +0 -1
- package/dist/routing/Route.d.ts.map +0 -1
- package/dist/routing/RouteContent.d.ts.map +0 -1
- package/dist/routing/RouteContentFunction.d.ts.map +0 -1
- package/dist/routing/RouteData.d.ts.map +0 -1
- package/dist/routing/RouteDataFunction.type.d.ts.map +0 -1
- package/dist/routing/RouteDataReturnType.d.ts.map +0 -1
- package/dist/routing/RouteParams.type.d.ts.map +0 -1
- package/dist/routing/RoutePaths.d.ts.map +0 -1
- package/dist/routing/RouteProp.d.ts.map +0 -1
- package/dist/routing/RouteType.d.ts.map +0 -1
- package/dist/routing/RouteTypes.d.ts.map +0 -1
- package/dist/routing/Router.d.ts.map +0 -1
- package/dist/routing/Routes.d.ts.map +0 -1
- package/dist/routing/RoutesFunction.d.ts.map +0 -1
- package/dist/routing/RoutesWithSafeParams.d.ts.map +0 -1
- package/dist/routing/RoutingRedirect.d.ts.map +0 -1
- package/dist/routing/Serializable.d.ts.map +0 -1
- package/dist/routing/TungstenErrorInfo.d.ts.map +0 -1
- package/dist/routing/WebRedirect.d.ts.map +0 -1
- package/dist/routing/builder.d.ts.map +0 -1
- package/dist/routing/builder.trial.d.ts.map +0 -1
- package/dist/routing/compareRequestToRegex.d.ts.map +0 -1
- package/dist/routing/debug.d.ts.map +0 -1
- package/dist/routing/defineRoutes.d.ts.map +0 -1
- package/dist/routing/getPathPriority.d.ts.map +0 -1
- package/dist/routing/matchPath.d.ts.map +0 -1
- package/dist/routing/patternToRegex.d.ts.map +0 -1
- package/dist/routing/pyramidRouting.d.ts.map +0 -1
- package/dist/routing/redirectOf.d.ts.map +0 -1
- package/dist/routing/registerGlobalData.d.ts.map +0 -1
- package/dist/routing/registerParts.types.test.d.ts +0 -2
- package/dist/routing/registerParts.types.test.d.ts.map +0 -1
- package/dist/routing/registerParts.types.test.js +0 -139
- package/dist/routing/registerRoutes.d.ts.map +0 -1
- package/dist/routing/registerRouting.d.ts.map +0 -1
- package/dist/routing/registerRouting.types.test.d.ts +0 -80
- package/dist/routing/registerRouting.types.test.d.ts.map +0 -1
- package/dist/routing/registerRouting.types.test.js +0 -120
- package/dist/routing/registrationTypes.d.ts.map +0 -1
- package/dist/routing/resolveAppRoutes.d.ts.map +0 -1
- package/dist/routing/routeSymbols.d.ts.map +0 -1
- package/dist/routing/routeUrl.d.ts.map +0 -1
- package/dist/routing/routesArray.d.ts.map +0 -1
- package/dist/routing/routesArray.types.test.d.ts +0 -2
- package/dist/routing/routesArray.types.test.d.ts.map +0 -1
- package/dist/routing/routesArray.types.test.js +0 -132
- package/dist/routing/routingBuilder.d.ts.map +0 -1
- package/dist/routing/routingBuilder.types.test.d.ts +0 -2
- package/dist/routing/routingBuilder.types.test.d.ts.map +0 -1
- package/dist/routing/routingBuilder.types.test.js +0 -171
- package/dist/routing/routingRegistry.d.ts.map +0 -1
- package/dist/routing/runContentFunction.d.ts.map +0 -1
- package/dist/routing/splitRequest.d.ts.map +0 -1
- package/dist/run.d.ts.map +0 -1
- package/dist/server.d.ts.map +0 -1
- package/dist/serverModuleLocation.d.ts.map +0 -1
- package/dist/serverRpc.d.ts.map +0 -1
- package/dist/serverRpcs.d.ts.map +0 -1
- package/dist/settings.d.ts.map +0 -1
- package/dist/signals/tracking.d.ts.map +0 -1
- package/dist/slash.d.ts.map +0 -1
- package/dist/spawnShellCommand.d.ts.map +0 -1
- package/dist/standardSchema.d.ts.map +0 -1
- package/dist/standardizeWebPath.d.ts.map +0 -1
- package/dist/startDevServer.d.ts.map +0 -1
- package/dist/stateTracking.d.ts.map +0 -1
- package/dist/staticGen/RouteDefinition.d.ts.map +0 -1
- package/dist/staticGen/getAppRoutes.d.ts.map +0 -1
- package/dist/staticGen/staticGen.d.ts.map +0 -1
- package/dist/staticGen/staticGenFunctions.d.ts.map +0 -1
- package/dist/style/jssToCss.d.ts.map +0 -1
- package/dist/style/styleContent.d.ts.map +0 -1
- package/dist/style/styleContentReact.d.ts.map +0 -1
- package/dist/style/styleContentReactClassOrClassname.d.ts.map +0 -1
- package/dist/style/styleContentSxy.d.ts.map +0 -1
- package/dist/style/styleContentSxyClassOrClassname.d.ts.map +0 -1
- package/dist/style/styleElementContent.d.ts.map +0 -1
- package/dist/style/unusedStyles.d.ts.map +0 -1
- package/dist/styleProcessors.d.ts.map +0 -1
- package/dist/symbols.d.ts.map +0 -1
- package/dist/sync/index.d.ts.map +0 -1
- package/dist/syncServer/index.d.cts.map +0 -1
- package/dist/syncServer/preload.d.ts.map +0 -1
- package/dist/testing/contentToString.d.ts.map +0 -1
- package/dist/testing/createTestApp.d.ts.map +0 -1
- package/dist/testing/display.d.ts.map +0 -1
- package/dist/testing/ensure.d.ts.map +0 -1
- package/dist/testing/ensure.trial.d.ts.map +0 -1
- package/dist/testing/ensure.types.test.d.ts +0 -2
- package/dist/testing/ensure.types.test.d.ts.map +0 -1
- package/dist/testing/ensure.types.test.js +0 -34
- package/dist/testing/helpers/contentHtmlElements.d.ts.map +0 -1
- package/dist/testing/host.d.ts.map +0 -1
- package/dist/testing/host.types.test.d.ts +0 -2
- package/dist/testing/host.types.test.d.ts.map +0 -1
- package/dist/testing/host.types.test.js +0 -81
- package/dist/testing/hydrateTestApp.d.ts.map +0 -1
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/installTestDocInit.d.ts.map +0 -1
- package/dist/testing/installTestStyleProcessors.d.ts.map +0 -1
- package/dist/testing/loadEnv.d.ts.map +0 -1
- package/dist/testing/normalize.d.ts.map +0 -1
- package/dist/testing/pipeline/createTestPacketApp.d.ts.map +0 -1
- package/dist/testing/pipeline/createTestPacketApp.types.test.d.ts +0 -3
- package/dist/testing/pipeline/createTestPacketApp.types.test.d.ts.map +0 -1
- package/dist/testing/pipeline/createTestPacketApp.types.test.js +0 -101
- package/dist/testing/pipeline/createTestPipelineApp.d.ts.map +0 -1
- package/dist/testing/pipeline/createTestPipelineApp.types.test.d.ts +0 -6
- package/dist/testing/pipeline/createTestPipelineApp.types.test.d.ts.map +0 -1
- package/dist/testing/pipeline/createTestPipelineApp.types.test.js +0 -146
- package/dist/testing/pipeline/hostPipeline.d.ts.map +0 -1
- package/dist/testing/pipeline/hydrateTestPipelineApp.d.ts.map +0 -1
- package/dist/testing/pipeline/hydrateTestPipelineApp.types.test.d.ts +0 -2
- package/dist/testing/pipeline/hydrateTestPipelineApp.types.test.d.ts.map +0 -1
- package/dist/testing/pipeline/hydrateTestPipelineApp.types.test.js +0 -27
- package/dist/testing/pipeline/index.d.ts.map +0 -1
- package/dist/testing/pipeline/pipelineTestApp.d.ts.map +0 -1
- package/dist/testing/pipeline/testPipelineRoute.d.ts.map +0 -1
- package/dist/testing/resolveContent.d.ts.map +0 -1
- package/dist/testing/testEnvIsRequested.d.ts.map +0 -1
- package/dist/testing/testingComponentPackage.d.ts.map +0 -1
- package/dist/throwError.d.ts.map +0 -1
- package/dist/timeProfiler.d.ts.map +0 -1
- package/dist/todo.d.ts.map +0 -1
- package/dist/update/UpdateResult.d.ts.map +0 -1
- package/dist/update/UpdateScheduler.d.ts.map +0 -1
- package/dist/update/guardedUpdate.d.ts.map +0 -1
- package/dist/update/integrateUpdateVdom.d.ts.map +0 -1
- package/dist/update/integrateUpdateVdomPrebuild.d.ts.map +0 -1
- package/dist/update/pendingMounts.d.ts.map +0 -1
- package/dist/validChildren.d.ts.map +0 -1
- package/dist/warnOnFrameworkChange.d.ts.map +0 -1
- package/dist/watch.d.ts.map +0 -1
- package/dist/when.d.ts.map +0 -1
- package/dist/zones/react.d.ts.map +0 -1
package/AGENTS.md
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
# Tungsten — guide for coding agents
|
|
2
|
+
|
|
3
|
+
A working guide to building applications with [Tungsten](https://robertsandiford.co.uk/tungsten),
|
|
4
|
+
written for AI coding agents. It covers the parts of the framework you need to write correct code,
|
|
5
|
+
and the places where Tungsten differs from React/Next in ways that silently produce broken output.
|
|
6
|
+
|
|
7
|
+
Read the section for the work you are about to do. The **Things to avoid** list at the end is a
|
|
8
|
+
summary, not a substitute.
|
|
9
|
+
|
|
10
|
+
## Authority
|
|
11
|
+
|
|
12
|
+
Prefer these sources in order:
|
|
13
|
+
|
|
14
|
+
1. The installed source and TypeScript declarations — `node_modules/tungsten/dist/**/*.d.ts`, and
|
|
15
|
+
`src/` if you have the framework repo. This is definitive.
|
|
16
|
+
2. Existing code in the application you are working on. Follow its established patterns rather
|
|
17
|
+
than introducing a second way of doing the same thing.
|
|
18
|
+
3. This document.
|
|
19
|
+
4. The [published documentation](https://robertsandiford.co.uk/tungsten). Good for concepts and
|
|
20
|
+
background, but it is a work in progress and some examples use older API names.
|
|
21
|
+
|
|
22
|
+
If this document and the installed declarations disagree, the declarations win — say so rather
|
|
23
|
+
than working around it.
|
|
24
|
+
|
|
25
|
+
## What Tungsten is
|
|
26
|
+
|
|
27
|
+
A TypeScript, class-component frontend framework whose rendering and updating algorithm is
|
|
28
|
+
deliberately close to React's, wrapped in a scope closer to Next: it supplies routing, scoped
|
|
29
|
+
styling, a reactive store, SSR + hydration, environment handling, dev/prod servers, and a build
|
|
30
|
+
pipeline.
|
|
31
|
+
|
|
32
|
+
What it is **not**:
|
|
33
|
+
|
|
34
|
+
- Not React. Do not add React, React DOM, or hooks-based code to a Tungsten app. Some
|
|
35
|
+
function-component compatibility exists (`src/hooks/`, `src/compat/`) but it is deprioritized —
|
|
36
|
+
function-component stand-ins are not persistent. Class components are the model.
|
|
37
|
+
- Not file-system routed. Routes are defined in code, deliberately.
|
|
38
|
+
- Not a full-stack data framework. It has no server components and no opinion about your API or
|
|
39
|
+
database. tRPC and Prisma appear in the author's own applications by preference; they are not
|
|
40
|
+
part of Tungsten.
|
|
41
|
+
|
|
42
|
+
## Related packages
|
|
43
|
+
|
|
44
|
+
Two sibling repositories, in `framework/` next to this work folder, ship with Tungsten and track
|
|
45
|
+
its conventions:
|
|
46
|
+
|
|
47
|
+
- **`create-tungsten-app`** — the project generator, with the starter templates under `templates/`.
|
|
48
|
+
A change to an app-facing protocol here (config, env, routing, the JSX runtime) usually has to
|
|
49
|
+
reach the templates too.
|
|
50
|
+
- **`eslint-config-tungsten-flat`** — the lint configuration the generated apps use.
|
|
51
|
+
|
|
52
|
+
Neither is touched from this repo; each has its own working tree and history.
|
|
53
|
+
|
|
54
|
+
## Runtime model
|
|
55
|
+
|
|
56
|
+
On an initial request the server matches a route, runs that route's `data` function, renders the
|
|
57
|
+
root component and matched page to HTML, and sends it. The browser hydrates that markup.
|
|
58
|
+
Subsequent navigation through `<Link>` is client-side: the destination route's `data` function is
|
|
59
|
+
fetched first, then its content renders.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
request -> match route -> route data -> root + page render -> HTML -> hydrate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Two consequences that cause real bugs:
|
|
66
|
+
|
|
67
|
+
- **The server render and the hydration render must produce identical output.** Anything
|
|
68
|
+
environment-dependent — `toLocaleString`, local timezone, `Math.random`, `Date.now` — will
|
|
69
|
+
differ between server and browser and corrupt hydration. Build such strings from fixed parts
|
|
70
|
+
(UTC getters, explicit locales), and coerce defensively: a `Date` in route data may arrive at
|
|
71
|
+
the client as an ISO string.
|
|
72
|
+
- **Clicks before hydration completes are lost.** This is the standard SSR caveat; do not add
|
|
73
|
+
application code to work around it.
|
|
74
|
+
|
|
75
|
+
## Project layout and configuration
|
|
76
|
+
|
|
77
|
+
`tungsten.config.js` at the project root configures the framework. Defaults are in
|
|
78
|
+
`src/defaultConfig.ts`; the type is `FrameworkConfig` (usable via JSDoc in a `.js` config).
|
|
79
|
+
|
|
80
|
+
| Key | Meaning |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| `codeDir` | Folder of compiled application code Tungsten consumes, e.g. `'dist'`. |
|
|
83
|
+
| `sourceDir` | Source folder — needed to locate `.sass`/`.css` files if the TS build does not copy them. |
|
|
84
|
+
| `rootComponent` | Entry component, relative to `codeDir`, e.g. `'client/Root.jsx'`. |
|
|
85
|
+
| `routesLocation` | `[file, exportName]`, relative to `codeDir`. Defaults to `['routes.jsx', 'routes']`; set it only when the routes live elsewhere or under another name, e.g. `['client/routes.jsx', 'getRoutes']`. |
|
|
86
|
+
| `envSchemasLocation` | Module that registers the env schemas (see [Environment](#environment)). Defaults to `'env.js'`, e.g. `'common/env.js'`. |
|
|
87
|
+
| `clientDirs` / `clientPatterns` | Sources the browser pipeline compiles. Unset, everything. |
|
|
88
|
+
| `rawDirs` / `rawPatterns` | Sources the node pipeline compiles (types stripped, imports untouched) — server middleware and the like. Unset, nothing. |
|
|
89
|
+
| `ignoreDirs` / `ignorePatterns` | Sources neither pipeline compiles. No default. Each pipeline compiles its selection **and not** these, so an ignored folder cannot be selected back in. `ignoreDirs: ['server']` suits an app whose own build emits its server code; to have Tungsten compile the server, narrow `clientDirs` and select it in `rawDirs` instead. |
|
|
90
|
+
| `serverMiddleware` | `[file, exportName, mountPath]` — Express handlers mounted on Tungsten's own server, so a frontend and an API can share one process. Loaded from the raw pipeline's output when the file is in `rawDirs`, otherwise from `codeDir`. |
|
|
91
|
+
| `defaultCacheControl` | The `Cache-Control` of prod pages that set none themselves (see [Page caching](#page-caching)). Unset, pages are sent without one. |
|
|
92
|
+
| `serverSideRouting` | 0.7.1+. Unset is `true`. `false`: the server runs none of the app's code for a page. It does no routing and fetches no data, and sends the same stub for every URL (status 200): the root component's `<html>`, `<head>` and `<body>` with nothing in them. The client then fetches the global data (or the pipeline's all-routes data) beside its first routing, with the route data, and builds the head and body. A pipeline app runs, in order: `clientInit`/`init`, then the all-routes data beside routing, then `clientPost`/`post`, then `clientAfterRouting`. A route data redirect is followed on the client before anything is built. Turns `csr` on, and needs a root component. The root's own `content` still runs on the server, and registered stores are built there with an empty context, so neither may depend on the data. Static generation writes the stub for each listed route; routes found only by following links are not found. |
|
|
93
|
+
|
|
94
|
+
### The two-stage build, and why imports end in `.jsx`
|
|
95
|
+
|
|
96
|
+
The application `tsconfig.json` uses `jsx: "preserve"` with an `outDir`, so **tsc emits `.jsx`
|
|
97
|
+
files**; Tungsten then Babel-processes those (its own JSX runtime plus transforms for component
|
|
98
|
+
state, bindings, styles, css imports, and package handling).
|
|
99
|
+
|
|
100
|
+
Under NodeNext resolution that means source files import the *emitted* extension:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { Home } from './pages/Home.jsx' // even though the file is Home.tsx
|
|
104
|
+
import { formatDate } from './lib/date.js' // even though the file is date.ts
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This is correct and intentional. Do not "fix" it to `.tsx`/`.ts` or extensionless.
|
|
108
|
+
|
|
109
|
+
Typical scripts: `dev` = `tsc --watch` alongside `tungsten dev`; `build` = `tsc && tungsten build`;
|
|
110
|
+
`start` = `tungsten start`.
|
|
111
|
+
|
|
112
|
+
## Components
|
|
113
|
+
|
|
114
|
+
Base classes, exported from `tungsten` with both a long and a short name
|
|
115
|
+
(`src/Components.tsx`, `src/indexInner.ts`):
|
|
116
|
+
|
|
117
|
+
| Use | Class | Alias |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| No local state | `TungstenSimpleComponent<Props, RouteData?>` | `SimpleComponent` |
|
|
120
|
+
| Local state | `TungstenStatefulComponent<Props, State, RouteData?>` | `StatefulComponent` |
|
|
121
|
+
| Optional state | `TungstenComponent<Props, State?, RouteData?>` | — |
|
|
122
|
+
| Store access, stateless | `TungstenBaseSimpleStoreComponent<Props, RouteData, Schema>` | `BaseSimpleStoreComponent` |
|
|
123
|
+
| Store access, stateful | `TungstenBaseStatefulStoreComponent<Props, State, RouteData, Schema>` | `BaseStatefulStoreComponent` |
|
|
124
|
+
|
|
125
|
+
Applications normally subclass the two store base classes once with their own schema and use those
|
|
126
|
+
wrappers everywhere (see **Store**). Pick the smallest class that fits: no state where none is
|
|
127
|
+
needed, no store where the component only needs props.
|
|
128
|
+
|
|
129
|
+
Everything is a class field, usually an arrow function:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { StatefulComponent, BindableInput } from 'tungsten'
|
|
133
|
+
|
|
134
|
+
type Props = { initialName: string }
|
|
135
|
+
type State = { name: string }
|
|
136
|
+
|
|
137
|
+
export class Greeting extends StatefulComponent<Props, State> {
|
|
138
|
+
state: State = { name: this.props.initialName }
|
|
139
|
+
|
|
140
|
+
title = `Hello, ${this.props.initialName}` // sets document title
|
|
141
|
+
|
|
142
|
+
reset = () => { this.state.name = this.props.initialName }
|
|
143
|
+
|
|
144
|
+
content = () => (
|
|
145
|
+
<section>
|
|
146
|
+
<BindableInput name="name" bind={[this.state, 'name']} />
|
|
147
|
+
<p>{this.state.name}</p>
|
|
148
|
+
<button onClick={this.reset}>Reset</button>
|
|
149
|
+
</section>
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
style = Sass`
|
|
153
|
+
section
|
|
154
|
+
padding: 1rem
|
|
155
|
+
`
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- **`content` is the render method.** It is called as `content(props, state)` and **may be
|
|
160
|
+
async** — the framework awaits it. (`render` is accepted as a React-compat alternative; if both
|
|
161
|
+
exist, `content` wins.)
|
|
162
|
+
- Use arrow-function class fields for handlers so `this` is bound.
|
|
163
|
+
- `title` sets the document title and may be `(props, state) => string`. **Only set `title` on
|
|
164
|
+
page components** — the last call wins, so a nested component setting it will fight the page.
|
|
165
|
+
- `this.tungsten` holds framework helpers bound to the component (0.7.1+):
|
|
166
|
+
`this.tungsten.getRoutes()` gives the app's routes as this request resolved them (the function
|
|
167
|
+
form called for you), for rendering nav; `this.tungsten.fileUrl('/logo.png')` gives a site
|
|
168
|
+
file's URL with its cache-busting version (see **Styling**). `this.tungsten.instance` is this
|
|
169
|
+
component's instance — the same object as `this.__instance`, under a name that is part of the
|
|
170
|
+
API rather than a convention.
|
|
171
|
+
|
|
172
|
+
### Lifecycle
|
|
173
|
+
|
|
174
|
+
Each hook has a React-style name and a short alias:
|
|
175
|
+
|
|
176
|
+
| Hook | Alias | Notes |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `init(props)` / `init(props, state)` | — | On construction, both server and client. May be async. A `TungstenSimpleComponent` is passed props only; the stateful bases pass state too. |
|
|
179
|
+
| `serverInit` / `clientInit` | — | Side-specific initialization. Same arguments as `init`. |
|
|
180
|
+
| `componentDidMount` | `_mounted` | |
|
|
181
|
+
| `componentDidHydrate` | `_hydrated` | Client only, after hydration attaches. |
|
|
182
|
+
| `componentDidUpdate` | `_updated` | |
|
|
183
|
+
| `componentDidRender` | `_rendered` | |
|
|
184
|
+
| `componentWillUnmount` | `_deleting` | |
|
|
185
|
+
| `componentDidUnmount` | `_deleted` | |
|
|
186
|
+
|
|
187
|
+
Put browser-only work (`window`, `location`, `localStorage`) in `clientInit` or a later client
|
|
188
|
+
hook — never unguarded in `init`, route code, or `content`.
|
|
189
|
+
|
|
190
|
+
Other instance members worth knowing: `this.props`, `this.routeParams`, `this.routeData`,
|
|
191
|
+
`this.globalData`, `this.triggerUpdate()` (manual re-render),
|
|
192
|
+
`this.pickProps(...)` / `this.pickState(...)`, and `this.using(disposable)` for something that
|
|
193
|
+
should be disposed on unmount. `this.setTimeout(fn, ms)` / `this.setInterval(fn, ms)` (with
|
|
194
|
+
`this.clearTimeout` / `this.clearInterval`) work like the window timers but are cleared when the
|
|
195
|
+
component is deleted; on the server they do nothing and return `undefined`.
|
|
196
|
+
|
|
197
|
+
### State is mutable and proxied — this is the biggest difference from React
|
|
198
|
+
|
|
199
|
+
`this.state`, and any plain objects, arrays, `Map`s and `Set`s inside it, are wrapped in proxies
|
|
200
|
+
on the client (`src/componentProxyHandlers.ts`). Writes are detected and queue a re-render.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
this.state.count++
|
|
204
|
+
this.state.items.push(item)
|
|
205
|
+
this.state.form.email = value
|
|
206
|
+
delete this.state.items[index]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
All of that is correct Tungsten code. Rules that follow from it:
|
|
210
|
+
|
|
211
|
+
- **Do not use `setState`.** It exists only as a React-compat shim and is not needed.
|
|
212
|
+
- **Do not write immutable-update code** — no `{ ...this.state, count: n }` ceremony, no
|
|
213
|
+
reducers. Deep state structures are fine and are not discouraged.
|
|
214
|
+
- **State updates apply immediately; re-renders are batched.** You read your own writes at once,
|
|
215
|
+
but the DOM catches up on the next tick. An `await` is a batching boundary — a queued update can
|
|
216
|
+
run while you are suspended.
|
|
217
|
+
- **Never mutate state inside `content`** or anything it calls. That risks a torn render. Mutate
|
|
218
|
+
in init hooks and event handlers only.
|
|
219
|
+
- An object already owned by another component's state is **adopted by copy** when you store it,
|
|
220
|
+
so the two components' states stay independent. Do not rely on sharing a mutable object between
|
|
221
|
+
components as a communication channel — use the store, or props plus a callback.
|
|
222
|
+
- **An `async content` widens every one of these rules.** Nothing locks a component while it is
|
|
223
|
+
suspended: a state write starts a second, overlapping update, and a parent re-render replaces
|
|
224
|
+
`this.props` under the render in flight — so code before the first `await` and code after it can
|
|
225
|
+
see different props. Prefer a synchronous `content` with the loading done in a lifecycle hook.
|
|
226
|
+
|
|
227
|
+
#### Framework internals: the render baseline is stamped *before* the render
|
|
228
|
+
|
|
229
|
+
`markRendered(element)` records the tick a component last rendered at, and `propsAreEqual()` skips
|
|
230
|
+
a child whose props carry no write newer than it. Both call sites in `integrateUpdateVdom.ts` must
|
|
231
|
+
therefore stamp **before** building content, as `__initialContent` already does at mount. Stamping
|
|
232
|
+
afterwards buries any write that landed while an async content was suspended: its tick is then
|
|
233
|
+
`<=` the baseline, and the child is skipped **for good**, since nothing will stamp that object
|
|
234
|
+
again. Regression test: `src/update/asyncRenderBaseline.dom.sxyt.tsx`.
|
|
235
|
+
|
|
236
|
+
## Routing and data loading
|
|
237
|
+
|
|
238
|
+
Routes are an object keyed by path, exported from the module named by `routesLocation` — by
|
|
239
|
+
default an export named `routes` from `routes.jsx` in `codeDir`, which is what
|
|
240
|
+
`create-tungsten-app` generates and needs no config:
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
export const routes = {
|
|
244
|
+
'/': { content: <Home /> },
|
|
245
|
+
'404': { content: <NotFound /> },
|
|
246
|
+
} as const satisfies CustomRoutes<Endpoint<any, any>>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The export may instead be a function of the request, `(reqRes: ReqRes | null) => Routes`, when
|
|
250
|
+
building the routes needs it — below, to create an API client for that request. A different
|
|
251
|
+
export name must be set in `routesLocation`; a missing export throws an error naming the setting.
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { type Params, type Endpoint, type CustomRoutes } from 'tungsten'
|
|
255
|
+
|
|
256
|
+
// tungsten.config.js: routesLocation: ['routes.jsx', 'getRoutes']
|
|
257
|
+
export const getRoutes = (reqRes: ReqResOrNull) => {
|
|
258
|
+
const api = getApiClient(reqRes)
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
'/': { content: <Home /> },
|
|
262
|
+
|
|
263
|
+
'/things/:id<1+>': {
|
|
264
|
+
data: ({ id }: Params<'id'>) => api.thing.get(Number(id)),
|
|
265
|
+
content: (data, params: Params<'id'>) => <ThingDetail thing={data} />,
|
|
266
|
+
} satisfies Endpoint<Params<'id'>, ThingResponse>,
|
|
267
|
+
|
|
268
|
+
'404': { content: <NotFound /> },
|
|
269
|
+
} as const satisfies CustomRoutes<Endpoint<any, any>>
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export type AppRoutes = ReturnType<typeof getRoutes>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- `content` is JSX, or `(data, params) => JSX` when the route has a `data` function.
|
|
276
|
+
- `data` is async and runs **before** the content renders — on the server during SSR, over the
|
|
277
|
+
network on client-side navigation. The component then receives resolved data, so there is no
|
|
278
|
+
loading flash on first paint. Route loaders are the right place for a page's initial read.
|
|
279
|
+
- Annotate each entry with `satisfies Endpoint<Params, Data>`. `Data` should be the actual response
|
|
280
|
+
type of the endpoint, not a locally duplicated shape. `satisfies` preserves the literal path key
|
|
281
|
+
while still checking the loader and the content callback.
|
|
282
|
+
- A **`'404'` key** supplies not-found content. Every application needs one. It answers with status
|
|
283
|
+
404, or with its own **`code`** when it gives one (0.7.1+), e.g. `'404': { content: <Gone />,
|
|
284
|
+
code: 410 }`. An object typed with `CustomRoutes` accepts `code` from unreleased; in 0.7.1 only
|
|
285
|
+
`Routes` and the array form do.
|
|
286
|
+
- Route definitions may carry **arbitrary extra fields** (`name`, `nav`, `requireAuth`, …) by
|
|
287
|
+
extending `Endpoint<Params, Data>` — a navigation component can then read them off the routes
|
|
288
|
+
object.
|
|
289
|
+
|
|
290
|
+
### The array form (0.7.1+)
|
|
291
|
+
|
|
292
|
+
Routes may instead be an array, each entry naming its path — anywhere the object is accepted, and
|
|
293
|
+
as a routes function's return:
|
|
294
|
+
|
|
295
|
+
```tsx
|
|
296
|
+
import { route } from 'tungsten'
|
|
297
|
+
|
|
298
|
+
export const getRoutes = (reqRes: ReqResOrNull) => [
|
|
299
|
+
route({ path: '/', content: <Home /> }),
|
|
300
|
+
route({ path: '/things/:id<1+>', content: <Thing /> }),
|
|
301
|
+
route({ path: '404', content: <NotFound /> }),
|
|
302
|
+
]
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- `route()` keeps the path literal, so `RoutesType` and `PathsType` read the array as they read
|
|
306
|
+
the object.
|
|
307
|
+
- A **path given twice** is a type error when the routes are registered through the app builder's
|
|
308
|
+
`.routes()`: the message names the path. It needs the array written out in the call (or in the
|
|
309
|
+
function it returns from); an array built elsewhere is checked when the routes are read instead,
|
|
310
|
+
which throws in dev. Outside dev, the last entry for a path wins, as a repeated object key does.
|
|
311
|
+
- The array cannot carry the object's `[$cacheControl]`; each entry's own `cacheControl` still
|
|
312
|
+
applies.
|
|
313
|
+
- **`routesWithList<Rule>()`** (0.7.1+) requires fields of every path route, leaving `'default'`,
|
|
314
|
+
`'404'` and `'error'` free of them. It returns the array as given, typed as a tuple:
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
export const routes = routesWithList<{ name: string }>()([
|
|
318
|
+
route({ path: '/', name: 'Home', content: <Home /> }),
|
|
319
|
+
route({ path: '404', content: <NotFound /> }),
|
|
320
|
+
])
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`routesWith<Rule>()` takes the routes as separate arguments and returns them keyed by path; its
|
|
324
|
+
rule applies to every route, `'404'` included.
|
|
325
|
+
|
|
326
|
+
### Path parameters
|
|
327
|
+
|
|
328
|
+
Express-like `:name` segments. Bare `:name` matches `[^/]+`; a trailing slash is always optional.
|
|
329
|
+
Two constraint syntaxes, both supported (`src/routing/patternToRegex.ts`):
|
|
330
|
+
|
|
331
|
+
- **Raw regex:** `:id(\d+)`.
|
|
332
|
+
- **Named types:** `:id<uint>`. Available: `digits`/`uint`/`unsigned-integer`;
|
|
333
|
+
`int`/`integer`/`sint`/`signed-integer`; `int-ge1`/`1+`; `num`/`number`; `alpha`;
|
|
334
|
+
`alpha-lower`; `alpha-num`/`alpha-numeric`; `alpha-lower-num`/`alpha-lower-numeric`.
|
|
335
|
+
|
|
336
|
+
Note `1+` means **an integer ≥ 1**, not "one or more digits". A route that does not match its
|
|
337
|
+
constraint falls through to the next candidate, so put literal paths before overlapping
|
|
338
|
+
parametric ones — `/things/new` must precede `/things/:id<1+>`.
|
|
339
|
+
|
|
340
|
+
Type params with `Params<'id'>` or `ParamsFromPath<'/things/:id'>`.
|
|
341
|
+
|
|
342
|
+
### Navigation and redirects
|
|
343
|
+
|
|
344
|
+
- `<Link to="/things/1">` for internal navigation — it keeps routing client-side. Use a plain
|
|
345
|
+
`<a>` only for external URLs or paths deliberately served by something else. Since 0.6.2 a click
|
|
346
|
+
with a modifier key (ctrl, meta, shift, alt) or a non-primary button is left to the browser, so
|
|
347
|
+
open-in-new-tab works.
|
|
348
|
+
- **Typed links (0.7.1+):** `RoutePaths<typeof routes>` is the union of the app's paths,
|
|
349
|
+
parameters as template holes: `'/leads/:id<1+>'` gives `` `/leads/${number}` ``. The routes may be
|
|
350
|
+
an object or a routes function (every table it can return counts). Make a `Link` typed by it in
|
|
351
|
+
an app file:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { makeTypedLink } from 'tungsten'
|
|
355
|
+
import type { routes } from '../routes.jsx'
|
|
356
|
+
|
|
357
|
+
export const { Link } = makeTypedLink('Link')<typeof routes>()
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
The string names the component for stylesheets (`Link` as a selector), and it is the only key
|
|
361
|
+
returned, so destructuring any other name is a type error. The type argument is the routes
|
|
362
|
+
(object or function), not `RoutePaths`: leaving it out, or passing anything with no paths, fails
|
|
363
|
+
at this call. Import the routes' type with
|
|
364
|
+
`import type`: a value import of the routes file from a component risks an import cycle. A plain
|
|
365
|
+
subclass does the same, its class name being the selector:
|
|
366
|
+
`export class Link extends _Link<RoutePaths<typeof routes>> {}`.
|
|
367
|
+
- `this.__instance.goto(path)` navigates from code; `<GoTo to="/path" />` does it when rendered.
|
|
368
|
+
Since 0.6.2, with `clientSideNav: false` it is a full page load, and during a server render it
|
|
369
|
+
answers the request with a redirect immediately (the first one wins if the render asks twice).
|
|
370
|
+
- Back/Forward keep the query (0.7.1+): a route's `data()` gets the query of the entry moved
|
|
371
|
+
to, and `internalStore.location` is the route path plus query after hydration as on the server.
|
|
372
|
+
Moving back from a same-page `#anchor` no longer re-runs the route or reloads.
|
|
373
|
+
- From a `data` function, **return** `new RoutingRedirect(path)` (in-app) or
|
|
374
|
+
`new WebRedirect(url)` (full browser redirect) instead of data. `RedirectCode` sets the status.
|
|
375
|
+
- As content, render `<RouteRedirectComponent to="/path" />`.
|
|
376
|
+
- `<MatchedRoute />` renders the active route; it belongs in the root component's `<body>`.
|
|
377
|
+
|
|
378
|
+
### Page caching
|
|
379
|
+
|
|
380
|
+
The prod server sends each page with a `Cache-Control` chosen from named policies (0.7.1+):
|
|
381
|
+
|
|
382
|
+
| Value | Header | Use for |
|
|
383
|
+
| --- | --- | --- |
|
|
384
|
+
| `'no-store'` | `no-store` | pages that must never be kept |
|
|
385
|
+
| `'private-check'` | `private, no-cache` | per-user pages: only the user's browser keeps them, and checks back each time (a cheap 304 when unchanged) |
|
|
386
|
+
| `'public-check'` | `public, no-cache` | pages the same for everyone: CDNs may keep them too, still checking back each time |
|
|
387
|
+
| `{ privacy, maxAge, staleWhileRevalidate? }` | e.g. `public, max-age=300` | reused without checking for `maxAge` seconds; `privacy` is `'private'` or `'public'` |
|
|
388
|
+
|
|
389
|
+
`public` lets a CDN serve one user's copy to another, so never use it on a page that shows
|
|
390
|
+
per-user data. A `maxAge` page can be up to that many seconds out of date, including after a
|
|
391
|
+
deploy.
|
|
392
|
+
|
|
393
|
+
The most specific setting wins:
|
|
394
|
+
|
|
395
|
+
```ts
|
|
396
|
+
import { $cacheControl } from 'tungsten'
|
|
397
|
+
|
|
398
|
+
export const routes = {
|
|
399
|
+
[$cacheControl]: 'private-check', // every route that sets none
|
|
400
|
+
'/docs': { content: <Docs />, cacheControl: 'public-check' },
|
|
401
|
+
'/feed': {
|
|
402
|
+
content: <Feed />,
|
|
403
|
+
cacheControl: ({ req }) => (req.query.live !== undefined ? 'no-store' : 'public-check'),
|
|
404
|
+
},
|
|
405
|
+
// ...
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
A route's `cacheControl` (a value, or a function of
|
|
410
|
+
`{ req, res }`) beats the routes object's `[$cacheControl]`, which beats config
|
|
411
|
+
`defaultCacheControl`. With none of them set no header is
|
|
412
|
+
sent, and a `Cache-Control` set by `serverMiddleware` is never overwritten. The error page and
|
|
413
|
+
redirects are not affected. Static sites take their headers from their host.
|
|
414
|
+
|
|
415
|
+
### The root component
|
|
416
|
+
|
|
417
|
+
The root component replaces `index.html` — it renders `<html>`, `<head>` and `<body>` itself.
|
|
418
|
+
|
|
419
|
+
**`<head>` content is not hydrated.** Event handlers placed there are never attached. The
|
|
420
|
+
framework inserts its own scripts and stylesheets into the head.
|
|
421
|
+
|
|
422
|
+
## Store and global data
|
|
423
|
+
|
|
424
|
+
`registerStore(SchemaClass)` creates a typed reactive store. The schema is a **class**,
|
|
425
|
+
constructed per app instance with the request context, which is what makes it safe under SSR —
|
|
426
|
+
one store per request on the server, not a module-level singleton.
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
// Store.ts
|
|
430
|
+
export class StoreSchema {
|
|
431
|
+
apiClient: ApiClient
|
|
432
|
+
authed: boolean = false
|
|
433
|
+
constructor(reqRes: ReqRes | null) { this.apiClient = getApiClient(reqRes) }
|
|
434
|
+
}
|
|
435
|
+
export const { getComponentStore } = registerStore(StoreSchema)
|
|
436
|
+
|
|
437
|
+
// StoreComponents.ts — subclass once, use everywhere
|
|
438
|
+
export abstract class SimpleStoreComponent<P, RD>
|
|
439
|
+
extends BaseSimpleStoreComponent<P, RD, StoreSchema> {
|
|
440
|
+
store = getComponentStore(this)
|
|
441
|
+
}
|
|
442
|
+
export abstract class StatefulStoreComponent<P, S, RD = unknown>
|
|
443
|
+
extends BaseStatefulStoreComponent<P, S, RD, StoreSchema> {
|
|
444
|
+
store = getComponentStore(this)
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Each store value exposes `.get()`, `.set(v)`, `.subscribe()`, `.unsubscribe()`, `.sync()`, and —
|
|
449
|
+
since 0.6.1 — `.notify(fn)` and `.stopNotify(fn)`.
|
|
450
|
+
|
|
451
|
+
**`sync()` = get + subscribe this component.** Use `sync()` when reading from `content` and the
|
|
452
|
+
component should re-render on change; use `get()` for a one-off read, such as pulling a client out
|
|
453
|
+
to make a call. Subscriptions are cleaned up when the component is deleted.
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
const client = this.store.apiClient.get() // just need the object
|
|
457
|
+
const authed = this.store.authed.sync() // re-render when this changes
|
|
458
|
+
this.store.authed.set(true)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**`notify(fn)` runs a callback on every write, instead of re-rendering.** Use it for a side effect
|
|
462
|
+
a value should trigger — closing a menu, starting a timer, calling out — where `sync()` would be
|
|
463
|
+
wrong because nothing in the markup depends on the value. The callback receives the new value, and
|
|
464
|
+
`notify` returns the function so it can be kept for `stopNotify(fn)`.
|
|
465
|
+
|
|
466
|
+
```tsx
|
|
467
|
+
init = () => {
|
|
468
|
+
this.store.authed.notify(authed => { if (!authed) this.closeMenu() })
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Callbacks registered from a component are released when that component is deleted, like
|
|
473
|
+
subscriptions — there is no need to call `stopNotify` in a teardown hook. `notify` runs on writes
|
|
474
|
+
only; it does not fire with the current value on registration.
|
|
475
|
+
|
|
476
|
+
The framework's own store is reached through `this.__instance.internalStore`. Its values are not
|
|
477
|
+
bound to a component, so pass it — from a store component, since that is the type they take:
|
|
478
|
+
`.sync(this)`, `.notify(fn, this)`. Two of them are read-only
|
|
479
|
+
(0.6.2+): `navigating` (a client navigation is in progress) and `navigationIndex` (counts
|
|
480
|
+
navigations) have no `.set()` — `goto()` writes them, and a test harness can't supply them.
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
content = () => this.__instance.internalStore.navigating.sync(this) ? <Spinner /> : null
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
The store is for genuine cross-component state. Keep form and page state in `this.state`. Do not
|
|
487
|
+
push fetched page records into the store merely to avoid passing props — that is what route data
|
|
488
|
+
and props are for. The store may hold non-serializable services as well as data.
|
|
489
|
+
|
|
490
|
+
For request-scoped values available to every component, `registerGlobalData(async reqRes => ({…}))`
|
|
491
|
+
plus a declaration-merged `interface GlobalData` gives you `this.globalData`. The function runs
|
|
492
|
+
before any route renders: before SSR, with the request, and (0.7.1+) again before each
|
|
493
|
+
client-side navigation, with `reqRes` null, in parallel with the route's data. Its result replaces
|
|
494
|
+
the global data as the new route renders. The server's result is serialized into the page for
|
|
495
|
+
hydration, so it must be serializable.
|
|
496
|
+
|
|
497
|
+
**Context (0.7.1+).** `registerContext` supplies an object made fresh for each server request,
|
|
498
|
+
and once per page on the client (it survives client-side navigation):
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
registerContext({ client: ClientContext, server: ServerContext }) // new ServerContext(reqRes), new ClientContext()
|
|
502
|
+
registerContext(Context) // one class: new Context(reqRes | null)
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
Register it in code the routes file loads. The context is the routes function's second argument,
|
|
506
|
+
each store schema's fourth constructor argument (after `reqRes, globalData, matchedRouteInfo`), and
|
|
507
|
+
`this.__instance.context`. It is untyped (`unknown`) for now: annotate the parameter with your own
|
|
508
|
+
classes, e.g. `(reqRes, context: ClientContext | ServerContext) => routes`. Test harnesses take a
|
|
509
|
+
`context` option.
|
|
510
|
+
|
|
511
|
+
## Styling
|
|
512
|
+
|
|
513
|
+
Component styles are a class field set to a tagged template. **`Css`, `Sass` and `Scss` are
|
|
514
|
+
ambient globals** — declared in `tungsten/types/index.d.ts`, so there is no import. (Less is not
|
|
515
|
+
implemented.)
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
style = Sass`
|
|
519
|
+
.panel
|
|
520
|
+
display: grid
|
|
521
|
+
gap: 0.6rem
|
|
522
|
+
`
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Sass `@use` and imports of `.sass`/`.scss` files work inside the tag. For genuinely global
|
|
526
|
+
styles, import a `.sass` file — the import yields an href string — and render it as
|
|
527
|
+
`<Stylesheet>{baseStyles}</Stylesheet>` in the root's `<head>`, which emits a `<link>` and lets
|
|
528
|
+
the browser cache it. An inline `css="…"` attribute is also available on elements.
|
|
529
|
+
|
|
530
|
+
In a production build (0.7.1+) URLs to the site's own files get a `?v=<content hash>` query, and
|
|
531
|
+
those URLs are served with a one-year `immutable` cache. A new deploy changes the hash, so browsers
|
|
532
|
+
fetch the new file. It covers the framework's bundle and component CSS, and every file in `public/`,
|
|
533
|
+
the compiled stylesheets and `assets/`. For these it is automatic: any element's `src`, `href`,
|
|
534
|
+
`poster` or `srcset` naming one of those files exactly is versioned when rendered, so
|
|
535
|
+
`<Stylesheet>{baseStyles}</Stylesheet>` and a hand-written `<img src="/logo.png" />` both get it.
|
|
536
|
+
A URL built in code (a `fetch`, an inline style) gets it through
|
|
537
|
+
`this.tungsten.fileUrl('/logo.png')`, which also adds the site base path. Not covered: URLs that
|
|
538
|
+
already carry a `?` or `#`, and `url(...)` inside CSS. Dev URLs carry no version.
|
|
539
|
+
|
|
540
|
+
### How scoping works — and the mistake to avoid
|
|
541
|
+
|
|
542
|
+
Every selector in a component's `style` is rewritten to a class private to that component, and
|
|
543
|
+
that class is stamped onto the elements **the component itself renders**.
|
|
544
|
+
`dist/postcssPlugins/TransformCssNames.js` is the authority: a class selector `.panel` becomes
|
|
545
|
+
`Thing_panel`; a bare tag selector `div` becomes `Thing_#_div`.
|
|
546
|
+
|
|
547
|
+
Two things follow.
|
|
548
|
+
|
|
549
|
+
**First, tag selectors become class selectors, so a tag and a class have equal specificity** —
|
|
550
|
+
unlike ordinary CSS. Order and explicitness matter more than you expect.
|
|
551
|
+
|
|
552
|
+
**Second — the common error — a component cannot style markup rendered by a child component.**
|
|
553
|
+
This is deliberate encapsulation. React CSS Modules leak here; Tungsten does not.
|
|
554
|
+
|
|
555
|
+
```tsx
|
|
556
|
+
<nav className="user-links">
|
|
557
|
+
<Link to={`/users/${user.id}`}>Profile</Link>
|
|
558
|
+
</nav>
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
renders as `<nav class="user-links User_user-links"><a href="/users/3">Profile</a></nav>`. The
|
|
562
|
+
anchor belongs to `Link` and carries none of this component's classes, so:
|
|
563
|
+
|
|
564
|
+
```sass
|
|
565
|
+
// ❌ compiles to a selector that can never match — the links stay unstyled
|
|
566
|
+
.user-links
|
|
567
|
+
a
|
|
568
|
+
font-weight: 600
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
The rule is not an error, produces no warning, and simply does nothing. It is easy to write and
|
|
572
|
+
easy to miss in review, so check it deliberately whenever you style something that turns out to be
|
|
573
|
+
a component: `Link`, `BindableInput`, `BindableSelect`, `BindableTextarea`, and any component of
|
|
574
|
+
your own.
|
|
575
|
+
|
|
576
|
+
**The fix: name the component as the selector.** A capitalised name is transformed the same way a
|
|
577
|
+
tag is; Tungsten generates the scoped class and passes it to that component as `className`
|
|
578
|
+
automatically. There is no prop to write and no class name to invent.
|
|
579
|
+
|
|
580
|
+
```sass
|
|
581
|
+
// ✅
|
|
582
|
+
.user-links
|
|
583
|
+
Link
|
|
584
|
+
font-weight: 600
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
Passing a class explicitly — `<Link className="user-link">` — uses the same mechanism by hand.
|
|
588
|
+
Reserve it for cases the component selector cannot express: different styling for different
|
|
589
|
+
instances of the same component, or a child that also needs a global class.
|
|
590
|
+
|
|
591
|
+
Either way, **the child must accept a `className` prop and apply it**. Tungsten's own components
|
|
592
|
+
do. When you write a component, accept `className` and put it on your root element (or merge it
|
|
593
|
+
with `combineClassNames`) — that is what makes it styleable by a parent:
|
|
594
|
+
|
|
595
|
+
```tsx
|
|
596
|
+
type Props = { className?: string, label: string }
|
|
597
|
+
|
|
598
|
+
export class Badge extends SimpleComponent<Props> {
|
|
599
|
+
content = ({ className, label }: Props) =>
|
|
600
|
+
<span className={combineClassNames(className, 'badge')}>{label}</span>
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Parent rules that land this way still beat the child's own: component styles are emitted with
|
|
605
|
+
4× stacked class selectors (`.C.C.C.C`), so a parent overrides a child's internals. `!important`
|
|
606
|
+
is a last resort.
|
|
607
|
+
|
|
608
|
+
**Escape hatch:** `:i(<selector>)` — or its long form `:ignore(<selector>)` — pierces scoping:
|
|
609
|
+
`button :i(span)` reaches spans inside child components. Reserve it for third-party or legacy components that do not accept a `className`.
|
|
610
|
+
Reaching for it on a component you control means that component should have taken a `className`
|
|
611
|
+
instead.
|
|
612
|
+
|
|
613
|
+
### Class attribute
|
|
614
|
+
|
|
615
|
+
Both `class` and `className` work in JSX, but **`className` is the canonical internal prop** —
|
|
616
|
+
scoped styling reads and rewrites it, and the renderer converts it to the HTML `class` attribute.
|
|
617
|
+
Prefer `className` consistently, for native elements and components alike, so behaviour does not
|
|
618
|
+
change when an element becomes a component or a prop is forwarded. Build dynamic lists with
|
|
619
|
+
`combineClassNames`.
|
|
620
|
+
|
|
621
|
+
## Rendering a string of HTML
|
|
622
|
+
|
|
623
|
+
Use **`<ParseHtml>`** (the same component is also exported as `<RawHtml>`; prefer the `ParseHtml`
|
|
624
|
+
name, which says what it does). It parses the markup into an ordinary content tree, so the result
|
|
625
|
+
renders, hydrates and reconciles like anything else:
|
|
626
|
+
|
|
627
|
+
```tsx
|
|
628
|
+
<div className="tip-text"><ParseHtml html={tip.text} safe /></div>
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
| Prop | Effect |
|
|
632
|
+
| --- | --- |
|
|
633
|
+
| `html` | the markup. Anything unparseable is kept as text; it never throws |
|
|
634
|
+
| `safe` | applies `safeHtmlAllowList` — formatting, lists, tables, links, images |
|
|
635
|
+
| `allowList` | your own `HtmlAllowList`. Wins over `safe`. Disallowed elements are *unwrapped*, so no text is lost |
|
|
636
|
+
| `sanitize` | defaults to on: drops `<script>`, `javascript:`/`vbscript:` URLs, and `data:` URLs in navigation attributes. `on*` attributes are always dropped |
|
|
637
|
+
|
|
638
|
+
`parseHtml()`, `safeHtmlAllowList` and the `HtmlAllowList` type are exported for use outside a
|
|
639
|
+
component.
|
|
640
|
+
|
|
641
|
+
**The parsed elements are not part of any component's scoped CSS**, because no component rendered
|
|
642
|
+
them. Component styles reach them only through `:i()` / `:ignore()`:
|
|
643
|
+
|
|
644
|
+
```sass
|
|
645
|
+
.tip-text
|
|
646
|
+
:i(p)
|
|
647
|
+
margin: 0 0 0.5rem
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**Choosing a host element matters.** Authored HTML is usually block level — paragraphs, tables —
|
|
651
|
+
and a `<p>` host cannot contain it: the parser closes the host at the first inner block tag,
|
|
652
|
+
leaving it empty. That breaks hydration outright. Use a `div`.
|
|
653
|
+
|
|
654
|
+
`dangerouslySetInnerHTML={{ __html }}` also works, and hands the markup to the browser as the
|
|
655
|
+
element's inner content. Tungsten never reconciles inside it, so reach for it only when the markup
|
|
656
|
+
must not be touched — `<ParseHtml>` is the default choice.
|
|
657
|
+
|
|
658
|
+
### Markup that will be managed by hand: `<Protected>`
|
|
659
|
+
|
|
660
|
+
For an element whose contents a library or your own code will mutate directly (an editor, a chart,
|
|
661
|
+
a widget that owns its DOM), wrap the markup in **`<Protected>`**. It renders its children once and
|
|
662
|
+
Tungsten never updates them again: a parent re-render skips the subtree, so the reconciler does not
|
|
663
|
+
undo the manual changes. It does not stop the component's own updates, and it is a plain component,
|
|
664
|
+
so the initial markup still renders server-side and hydrates. Its `children` is a `ContentNode`, so
|
|
665
|
+
any JSX goes in without a cast — and markup you hold as a string goes in the same way through
|
|
666
|
+
`<ParseHtml>` or the `parseHtml()` function, which parse an HTML string into a content tree, the
|
|
667
|
+
same structure JSX compiles to:
|
|
668
|
+
|
|
669
|
+
```tsx
|
|
670
|
+
<Protected><ParseHtml html={widgetMarkup} /></Protected>
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Prefer it over the older `<NativeHtml>`, whose children type is `ContentNodeHtml` and therefore
|
|
674
|
+
needs a cast on any JSX child. The name is not final.
|
|
675
|
+
|
|
676
|
+
**Avoid `__noEscape`.** It is an older switch that turns off escaping for an element's children.
|
|
677
|
+
It inherits down the entire subtree, so nested Tungsten text also renders unescaped; it only ever
|
|
678
|
+
worked server-side, so a client render escapes the markup into visible text; and it leaks into the
|
|
679
|
+
markup as a literal `__noescape` attribute.
|
|
680
|
+
|
|
681
|
+
### Inline scripts and styles
|
|
682
|
+
|
|
683
|
+
The text children of `<script>` and `<style>` are emitted **verbatim** by the server renderer
|
|
684
|
+
(`renderingTreeToHtml.ts`; test `src/render/rawTextElements.sxyt.ts`). So inline code is just a
|
|
685
|
+
child string:
|
|
686
|
+
|
|
687
|
+
```tsx
|
|
688
|
+
<script>{themeBoot}</script>
|
|
689
|
+
<style>{criticalCss}</style>
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
The React idiom `<script dangerouslySetInnerHTML={{ __html: code }} />` exists because React
|
|
693
|
+
HTML-escapes script children; Tungsten does not, so the idiom is unnecessary — do not carry it
|
|
694
|
+
over. As with any framework, the string itself must not contain `</script>`.
|
|
695
|
+
|
|
696
|
+
A boot script that runs before hydration (theme, colour scheme) usually wants to set an attribute
|
|
697
|
+
on `<html>` or `<body>`. **Hydration reconciles attributes to what the component rendered and
|
|
698
|
+
removes any it did not** (`reconcileHydratedAttributes.ts`), so an attribute the script set on an
|
|
699
|
+
element the root component renders is stripped the moment that element hydrates — reported in dev
|
|
700
|
+
as a mismatch banner (`data-theme: rendered null, server sent …`), silently in prod. Deriving the
|
|
701
|
+
value on the client instead (`localStorage` in `clientInit`) does not help: the hydration render
|
|
702
|
+
then differs from the server's markup, which is the same mismatch from the other side, on every
|
|
703
|
+
load. The server has to know the value: **keep such a preference in a cookie**, read it
|
|
704
|
+
server-side (`reqRes` / global data) and render the attribute from the root component, so both
|
|
705
|
+
renders agree — no boot script, no flash, no mismatch. A value the server cannot know (`'auto'`,
|
|
706
|
+
i.e. `prefers-color-scheme`) should render no attribute at all, with CSS `@media
|
|
707
|
+
(prefers-color-scheme: dark)` as the fallback when `data-theme` is absent.
|
|
708
|
+
|
|
709
|
+
## Form binding
|
|
710
|
+
|
|
711
|
+
The bindable components give two-way binding to any object property:
|
|
712
|
+
|
|
713
|
+
```tsx
|
|
714
|
+
<BindableInput name="email" bind={[this.state.form, 'email']} />
|
|
715
|
+
<BindableSelect name="role" bind={[this.state.form, 'role']} options={roleOptions} />
|
|
716
|
+
<BindableCheckbox name="active" bindChecked={[this.state.form, 'active']} />
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
The family is `BindableInput`, `BindableInputTyped`, `BindableCheckbox`, `BindableRadio`,
|
|
720
|
+
`BindableSelect`, `BindableTextarea`. `bind={[object, 'key']}` accepts state, a nested state
|
|
721
|
+
object, or a plain instance property. Binds take an optional `filter` to transform or validate
|
|
722
|
+
incoming values before they reach the property — the usual use is converting an input string to
|
|
723
|
+
the field's real type.
|
|
724
|
+
|
|
725
|
+
`BindableSelect` takes either an `options` array of `{ value, text }` structs or ordinary JSX
|
|
726
|
+
children (`<option>`, `<optgroup>`, conditionals). Prefer `options` when the values form a literal
|
|
727
|
+
union: `value` is how option values reach the type system, and with children they degrade to
|
|
728
|
+
`string`. As with a plain `<select>`, `placeholder` does nothing — supply a leading empty-valued
|
|
729
|
+
option instead. See `src/components/bindable/BindableSelect.md` before changing that component.
|
|
730
|
+
|
|
731
|
+
A common and reasonable application pattern is to hold form values as strings in state and convert
|
|
732
|
+
to the domain type on submit, since browser inputs produce strings.
|
|
733
|
+
|
|
734
|
+
## Environment
|
|
735
|
+
|
|
736
|
+
Applications register Standard Schema schemas (zod, valibot, arktype…) from the module named by
|
|
737
|
+
`envSchemasLocation`: `registerPrivateEnvSchema(schema)` (server only) and
|
|
738
|
+
`registerPublicEnvSchema(schema)` (shipped to the client), both imported from `tungsten`; either
|
|
739
|
+
may be omitted. Tungsten imports that module for its side effect, loads `.env` files, validates
|
|
740
|
+
them at startup, and injects the public values into the client bundle. The old protocol of two
|
|
741
|
+
named exports, `privateEnvSchema` / `publicEnvSchema`, is no longer read and fails loudly.
|
|
742
|
+
|
|
743
|
+
Access is through **ambient globals**, no import: `tungstenPrivateEnv` and `tungstenPublicEnv`.
|
|
744
|
+
Type them by declaration-merging `TungstenPrivateEnv` / `TungstenPublicEnv` in `declare global`.
|
|
745
|
+
|
|
746
|
+
- Never expose a private value to client code, and never copy one into the public schema or file.
|
|
747
|
+
- Add every new variable to the appropriate schema and to the sample env file.
|
|
748
|
+
- Code that runs outside Tungsten startup (standalone scripts, some test setups) must bootstrap
|
|
749
|
+
the globals itself before importing modules that read them.
|
|
750
|
+
|
|
751
|
+
Tungsten reads four variables of its own, which your schemas must declare like any other:
|
|
752
|
+
`TUNGSTEN_SERVER_PORT` (private) and `FRONTEND_PROTO` / `FRONTEND_HOSTNAME` / `FRONTEND_PORT`
|
|
753
|
+
(public). Both ports are validated as numbers `>= 0`.
|
|
754
|
+
|
|
755
|
+
- `TUNGSTEN_SERVER_PORT` is the port the server binds; unset means 3000. **`0` asks the OS for a
|
|
756
|
+
free port** (0.6.0) and `tungsten dev` reports the one it took. `tungsten start`
|
|
757
|
+
rejects `0` — production needs a fixed address.
|
|
758
|
+
- The `FRONTEND_*` trio describes the address the **browser** uses, which is not the server's own
|
|
759
|
+
whenever a proxy sits in front. `FRONTEND_PORT` unset or `0` means "whatever port the server
|
|
760
|
+
bound", so a dev server on a random port still reports a usable URL. A default port is left out
|
|
761
|
+
of the URL: `https://site.example`, not `https://site.example:443`.
|
|
762
|
+
- **`FRONTEND_HOSTNAME` was `FRONTEND_HOST`** (0.6.0). Rename it in your env files
|
|
763
|
+
and schema — under the old name the public address silently falls back to `localhost`.
|
|
764
|
+
|
|
765
|
+
## Testing
|
|
766
|
+
|
|
767
|
+
**Write the failing test before the fix, and watch it fail.** Then fix, and watch it pass. A test
|
|
768
|
+
never seen red proves nothing — it may assert something that was already true, or never reach the
|
|
769
|
+
code at all. If you have already written the fix, revert it, run the test, confirm it fails for
|
|
770
|
+
the reason you expect, then restore it.
|
|
771
|
+
|
|
772
|
+
Two things bite here in particular. The runner executes compiled JS, not your source — an app's
|
|
773
|
+
test compile tree (`.tungsten/test` below), this repo's `dist/` — and a watch build may have
|
|
774
|
+
rebuilt your change already: check that the red run really is testing the unfixed code. And put the test at the layer the bug lives at: `createTestApp()` renders on the client
|
|
775
|
+
only, so a server-render or hydration bug can sit underneath a fully green suite.
|
|
776
|
+
|
|
777
|
+
Test APIs live at `tungsten/testing`, not the main entry point. The test project supplies its own
|
|
778
|
+
DOM implementation (happy-dom, jsdom):
|
|
779
|
+
|
|
780
|
+
```tsx
|
|
781
|
+
import { Window } from 'happy-dom'
|
|
782
|
+
import { createTestApp } from 'tungsten/testing'
|
|
783
|
+
|
|
784
|
+
const window = new Window()
|
|
785
|
+
const testApp = await createTestApp(RootComponent, {
|
|
786
|
+
document: window.document as unknown as Document,
|
|
787
|
+
props: { initialName: 'Robert' },
|
|
788
|
+
location: '/',
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
const button = testApp.container.querySelector('button')
|
|
793
|
+
button!.dispatchEvent(new window.Event('click') as unknown as Event)
|
|
794
|
+
await testApp.resolved()
|
|
795
|
+
expect(testApp.container.textContent).toContain('Robert')
|
|
796
|
+
} finally {
|
|
797
|
+
await testApp.unmount()
|
|
798
|
+
}
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
`createTestApp()` completes the initial client render before returning. `resolved()` then waits
|
|
802
|
+
for Tungsten updates, DOM reconciliation, lifecycle-triggered updates, and promises returned by
|
|
803
|
+
registered event handlers — but not unrelated timers or network requests. Casts are only needed
|
|
804
|
+
when the DOM package's types are not structurally compatible with the TypeScript DOM library.
|
|
805
|
+
An automatically created container is appended to the supplied document's body and removed by
|
|
806
|
+
`unmount()`; a caller-supplied container must be empty, and is emptied but kept. Both
|
|
807
|
+
`createTestApp()` and `hydrateTestApp()` are async-disposable (0.6.2+), so
|
|
808
|
+
`await using testApp = await createTestApp(...)` unmounts at the end of the scope instead of a
|
|
809
|
+
`try`/`finally`. Pass `routes` (an object, or a routes function — called with `null`, as on the
|
|
810
|
+
client) to let components `goto()` for real (0.6.2+); without it the app has no routes and a
|
|
811
|
+
navigation goes nowhere. Unlike an app's routes, these need no `'404'` or `'default'` route: a miss
|
|
812
|
+
lands on the built-in "404 page not found", which the test's own assertions will catch. Tests
|
|
813
|
+
containing Tungsten JSX must be compiled through `tungsten compile` so they use Tungsten's JSX runtime and
|
|
814
|
+
transforms.
|
|
815
|
+
|
|
816
|
+
`hydrateTestApp()` and `hydrateTestPipelineApp()` (0.7.1+) throw at once unless `isClient` is
|
|
817
|
+
true: run them in a DOM suite whose `setup` puts happy-dom's `window` on the global before the tests
|
|
818
|
+
are imported, since tungsten reads `isClient` when it loads.
|
|
819
|
+
|
|
820
|
+
An app built with `pipeline()` or `splitPipeline()` (0.7.1+) has its own pair, imported from
|
|
821
|
+
`tungsten/testing/pipeline` rather than `tungsten/testing`, taking the app (the routes module's `.routes()`
|
|
822
|
+
export, or a smaller set built off the same stage) first:
|
|
823
|
+
`createTestPipelineApp(app, Root, options)` runs the app's client stages and routes `location`
|
|
824
|
+
against its routes; `hydrateTestPipelineApp(app, Root, options)` runs the server stages (with a
|
|
825
|
+
stand-in request unless `reqRes` is given), renders, carries the all-routes and networked data as the
|
|
826
|
+
page does, then runs the client stages and hydrates. A component whose store or markup comes from
|
|
827
|
+
the pipeline's context needs these: `createTestApp()` and `hydrateTestApp()` run no pipeline, so its
|
|
828
|
+
context is empty.
|
|
829
|
+
|
|
830
|
+
Stores are created independently per test app; initialize one with `testStore()` before the root
|
|
831
|
+
component starts. Component tests that need the application's env globals opt in by calling
|
|
832
|
+
`loadEnv({ buildLocation })` once from the test runner `setup` hook, where `buildLocation` is the
|
|
833
|
+
test compile's output (`tungsten compile --output .tungsten/test` → `'.tungsten/test'`): the env
|
|
834
|
+
schemas module is imported from that compiled tree, not the dev build's. `loadEnv()` loads env once
|
|
835
|
+
per process and a missing/invalid env schema module is a hard error. `TUNGSTEN_TEST_ENV` (the
|
|
836
|
+
variable name is exported as `testEnvVariableName`) only chooses which files: set it to prefer
|
|
837
|
+
`.env.test*` over the base ones. The harness itself only waits for a load `setup` started
|
|
838
|
+
(`waitForEnvIfLoading`), so a package that never calls `loadEnv()` loads nothing.
|
|
839
|
+
|
|
840
|
+
### `host()` — a component without a DOM
|
|
841
|
+
|
|
842
|
+
`createTestApp()` is the harness for anything that needs a document. When the question is only
|
|
843
|
+
"what does this component render, and what do its handlers do", `host()` is the smaller tool: it
|
|
844
|
+
constructs the component with a mock construct package, awaits `init`, runs the mount lifecycles,
|
|
845
|
+
and hands back a wrapper.
|
|
846
|
+
|
|
847
|
+
```ts
|
|
848
|
+
import { host } from 'tungsten/testing'
|
|
849
|
+
|
|
850
|
+
const hosted = await host(DataTable, { head, rows: [], columns: 2, emptyText: 'Nothing found' })
|
|
851
|
+
|
|
852
|
+
hosted.content() // content(props) or content(props, state)
|
|
853
|
+
await hosted.updateProps({ ...props, rows }) // re-renders, runs the update lifecycles
|
|
854
|
+
await hosted.updateState(state => { state.open = true })
|
|
855
|
+
hosted.toggleRow(3) // the component's own methods, bound
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
- **`content()` takes no arguments.** It calls the content function with what that component's
|
|
859
|
+
flavour takes — `(props)` for a `TungstenSimpleComponent`, `(props, state)` for the rest — so
|
|
860
|
+
tests stop writing `component.content(component.props, component.state)` and stop casting when a
|
|
861
|
+
component's signature differs.
|
|
862
|
+
- **`content()` gives the whole tree, not one component's output** (the default since
|
|
863
|
+
0.7.1; before, `resolve: true` asked for it). Every component in the tree is constructed and
|
|
864
|
+
asked for its content, down to html and text, and `content()`, `updateProps()` and
|
|
865
|
+
`updateState()` all hand back that tree. Resolution is async, so they are promises whatever the
|
|
866
|
+
component's own `content` returns — await them — and the wrapper's type is
|
|
867
|
+
`HostedResolved<Component>`. **`resolve: false`** hands back what the hosted component itself
|
|
868
|
+
returned, its child components unexpanded (`one.component('UserCard')` is there, the html it
|
|
869
|
+
renders is not); that `content()` is not awaited — an async `content` returns a promise, a sync
|
|
870
|
+
one the node, and the type (`Hosted<Component>`) says which:
|
|
871
|
+
|
|
872
|
+
```ts
|
|
873
|
+
const full = await host(Page, { id: 7 })
|
|
874
|
+
ensure(await full.content(), [
|
|
875
|
+
ensure.one.component('UserCard').with.one.prop('name').is('Bob'), // node and props kept
|
|
876
|
+
ensure.one.html('em'), ensure.one.text('Bob'), // what it rendered
|
|
877
|
+
])
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
A component node stays in the tree as written — its type, key and props — holding what it
|
|
881
|
+
rendered in place of the children it was passed, so a fact reads it by name, by prop, and by what
|
|
882
|
+
it put on the page. A component that ignores its children shows none of them. Child components
|
|
883
|
+
run their real `init` and mount lifecycles against the same store, internal store and recorded
|
|
884
|
+
navigations — so pass the store the *children* need, even where the hosted component has none.
|
|
885
|
+
With a partial store, a key a child reads that the options did not supply names itself, as it
|
|
886
|
+
does for the hosted component, except in the child's own `init`, which has run by then. A
|
|
887
|
+
component the app renders on the client alone (`render`) resolves to its placeholder, its content
|
|
888
|
+
never run.
|
|
889
|
+
- **`contentToString(tree)` shows a tree as JSX** (0.7.1+, from `tungsten/testing`), to read
|
|
890
|
+
what a component rendered: `console.log(contentToString(await hosted.content()))`. A resolved
|
|
891
|
+
component's output sits under it, marked `⤵`; the children it was passed show as
|
|
892
|
+
`children={...}`, in full with `{ children: true }`. One unresolved nests its children as written.
|
|
893
|
+
A tree that renders nothing shows what it was (`{null}`, `{false}`, `{[]}`); inside a tree such
|
|
894
|
+
values are left out, as the page leaves them. Text with spaces at an edge is quoted (`{'Save '}`).
|
|
895
|
+
Fragments are hidden, their text running on as the page and `ensure()` text facts join it;
|
|
896
|
+
`{ fragments: true }` shows them:
|
|
897
|
+
|
|
898
|
+
```
|
|
899
|
+
<div id='page'>
|
|
900
|
+
<Middle name='Bob'> ⤵
|
|
901
|
+
<p>Hi, Bob.</p>
|
|
902
|
+
</Middle>
|
|
903
|
+
</div>
|
|
904
|
+
```
|
|
905
|
+
- **Nothing re-renders on its own.** The construct package is marked `hosted`, so state is not
|
|
906
|
+
proxied on either side; `updateProps` and `updateState` are the renders. `updateState` takes
|
|
907
|
+
`(state) => newState | void`, so mutate it or return a replacement.
|
|
908
|
+
- **No starting state and no app state.** The component inits and renders as it really would.
|
|
909
|
+
The internal store gets `location` (the option, else `/`) and empty `popMessages`, and a matched
|
|
910
|
+
route only if the options give one. Reading a key nothing
|
|
911
|
+
supplied throws, naming the key. If the component needs a real app, it needs `createTestApp()`.
|
|
912
|
+
- **The side is the process's.** `host()` has no client/server option: `isClient`/`isServer` are
|
|
913
|
+
what the framework and your components both read, so a plain node run is the server
|
|
914
|
+
(`serverInit`). To test the client, install a window and DOM (happy-dom, jsdom) in the test
|
|
915
|
+
runner's setup, before anything imports tungsten.
|
|
916
|
+
- `hosted.title` and `hosted.navigations` record what the component tried to do to the document
|
|
917
|
+
title and the router; both are recorded rather than carried out. A lifecycle failure is thrown
|
|
918
|
+
rather than logged, so a test cannot pass over one.
|
|
919
|
+
- **Literal props are typed by the component** (0.5.4+, also `createTestApp()`), so
|
|
920
|
+
`{ type: 'success' }` or `bind: [form, 'value']` needs no `as const`. Before 0.5.4 they widen to
|
|
921
|
+
`string` and arrays and fail against the component's props — annotate them. For a *generic*
|
|
922
|
+
component a bare tuple still widens in any version: annotate it
|
|
923
|
+
(`const bind: TypedBindVariable<Form, 'value'> = [form, 'value']`).
|
|
924
|
+
- **The third argument is the options** (`HostOptions<Component>`), required for a store
|
|
925
|
+
component. A component without a store of its own takes the store forms too (0.7.1+) — it
|
|
926
|
+
may hold children that have one, which resolving constructs against the same instance —
|
|
927
|
+
but with the schema unchecked, there being no class of its own to check it against. A store
|
|
928
|
+
component is given its store one of four ways: `store` (a real `makeStore` store, its cells
|
|
929
|
+
keeping state, so one can be shared between
|
|
930
|
+
hosts), `storeData` (a value per key, each wrapped in a cell of its own), or the partial opt-ins
|
|
931
|
+
`partialStore` / `partialStoreData`, where a key left out throws, naming itself, when read.
|
|
932
|
+
Routing: `location` (a `/path`), `routes`, and either `matchedRoute: { key, params?, data? }`
|
|
933
|
+
stated by the test, or `routing: true` to have `host()` match `location` against `routes`
|
|
934
|
+
itself. The location defaults to `/` (0.7.1+; before, it had none). There is no default
|
|
935
|
+
route: a component that reads one the test did not give throws, naming it. An internal-store component takes those routing options, not a store.
|
|
936
|
+
`HostOptions`, `HostStoreOptions`, `makeStore` and `TrueStore` are all exported from
|
|
937
|
+
`tungsten/testing`, so a test file needs no second import for them. For a wrapper that fixes
|
|
938
|
+
`resolve`, `HostResolveOptions` / `HostDontResolveOptions` (per component) and
|
|
939
|
+
`HostStoreResolveOptions` / `HostStoreDontResolveOptions` (per store schema) are the same
|
|
940
|
+
options with `resolve` settled (0.7.1+), so no distributive `Omit` is needed.
|
|
941
|
+
- **A real store for tests** is `makeStore(schema, new Map())`. The second argument is the
|
|
942
|
+
subscribed-elements map the store's cells share; a fresh `Map` is right for a test. Cells are
|
|
943
|
+
branded, so a hand-rolled `{ get, set, … }` object is not a `TrueStore` and will not type-check:
|
|
944
|
+
make one with `makeStore`, or give `host()` values through `storeData`. Stub what the schema
|
|
945
|
+
holds before handing it over, an API client above all:
|
|
946
|
+
|
|
947
|
+
```ts
|
|
948
|
+
const schema = new StoreSchema()
|
|
949
|
+
schema.apiClient = stubClient // a stub, not a real client
|
|
950
|
+
const store = makeStore(schema, new Map())
|
|
951
|
+
```
|
|
952
|
+
- **`serverInit` runs under `host()`.** A plain node run is the server, so a store component whose
|
|
953
|
+
`serverInit` calls the API reaches whatever client its store holds. Stub the client through the
|
|
954
|
+
store, as above.
|
|
955
|
+
- **Class components only.** `host()` takes a component class. A function component is a
|
|
956
|
+
function: call it.
|
|
957
|
+
- **`hostJsx()` takes the element instead** (0.7.1+), for a test that reads as the page does:
|
|
958
|
+
|
|
959
|
+
```ts
|
|
960
|
+
const hosted = await hostJsx(<DataTable head={head} rows={rows} columns={2} />)
|
|
961
|
+
ensure(await hosted.content(), [ensure.one.html('table')])
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
It is `host()` underneath, with the same options and the same lifecycles, given the class and
|
|
965
|
+
props off the element. The cost is types: TypeScript types every JSX expression as `JSX.Element`,
|
|
966
|
+
so the element does not carry the component it names, and the wrapper comes back as the base
|
|
967
|
+
component — `props` and `state` are `any`, the component's own methods are not on it, and
|
|
968
|
+
`content()` is `ContentNode | Promise<ContentNode>`, so `await` it. Naming the component in the
|
|
969
|
+
call restores all of that: `await hostJsx<DataTable>(<DataTable … />)`. Reach for `hostJsx()`
|
|
970
|
+
when the test asserts the content, and `host()` when it drives the component through the
|
|
971
|
+
wrapper's api. An element that is not a class component — an html tag, a fragment, a function
|
|
972
|
+
component — throws, naming what it was given. Also on `host.jsx()` (0.7.1+).
|
|
973
|
+
- **`host.generic()` for a component class that is generic** (0.7.1+). It takes the class in a
|
|
974
|
+
call of its own, then the props:
|
|
975
|
+
|
|
976
|
+
```ts
|
|
977
|
+
const hosted = await host.generic(BoundValue)({ bind: [form, 'value'] })
|
|
978
|
+
const own = await host.generic(BoundValue, { resolve: false })({ bind: [form, 'value'] })
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
Plain `host()` cannot infer such a class's type arguments: passed as an argument, its own type
|
|
982
|
+
parameters are erased to their constraints before the props are looked at, so a tuple prop like
|
|
983
|
+
`bind: [form, 'value']` widens to an array and the class stops fitting — `Target requires 2
|
|
984
|
+
element(s) but source may have fewer`. Annotating the prop (`const bind: TypedBindVariable<Form,
|
|
985
|
+
'value'> = [form, 'value']`) is the way round it with `host()`; `host.generic()` needs no
|
|
986
|
+
annotation and checks the props against the component the way JSX does. Options go on the first
|
|
987
|
+
call, not the second — that is what keeps the inference working.
|
|
988
|
+
- **A generic helper can wrap it without a cast.** Store components resolve to an
|
|
989
|
+
overload whose schema is its own type parameter, inferred from the class, so a project helper
|
|
990
|
+
such as `<C extends TungstenBaseComponent<any, any> & { store: ComponentStore<Schema> }>(Type,
|
|
991
|
+
props, store) => host(Type, props, { store })` type-checks while `C` is still generic. The
|
|
992
|
+
store options are checked as before, and a wrong schema or a store-less component is rejected
|
|
993
|
+
at the `host()` call inside the helper. `HostStoreOptions<Schema>` is the options type for a
|
|
994
|
+
schema, for a helper that builds on it, say making `store` optional with a mock default. It is
|
|
995
|
+
a union (the four store forms times the two routing forms), so `Omit` it through a conditional,
|
|
996
|
+
which distributes; a plain `Omit<HostStoreOptions<S>, 'store'>` merges the routing variants,
|
|
997
|
+
`routing` widens to `boolean`, and `host()` rejects the result:
|
|
998
|
+
|
|
999
|
+
```ts
|
|
1000
|
+
type OptionalStore<V> = V extends unknown ? Omit<V, 'store'> & { store?: TrueStore<S> } : never
|
|
1001
|
+
type Options = OptionalStore<Extract<HostStoreOptions<S>, { store: unknown }>>
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
Before this, `host()`'s argument tuple was conditional on the component, which a still-generic
|
|
1005
|
+
component cannot satisfy, so such helpers cast.
|
|
1006
|
+
|
|
1007
|
+
### `ensure()` — facts about a content tree
|
|
1008
|
+
|
|
1009
|
+
`ensure()` states what a content tree holds, with no DOM and no string of HTML in between
|
|
1010
|
+
(0.7.1+). It takes the tree and a list of facts: a passing call is its own assertion, and a
|
|
1011
|
+
failure throws `EnsureError`, naming the fact and what it saw, with the `actual` / `expected` /
|
|
1012
|
+
`showDiff` fields a runner reads for a diff and a stack that starts at the `ensure()` call.
|
|
1013
|
+
|
|
1014
|
+
```ts
|
|
1015
|
+
import { ensure } from 'tungsten/testing'
|
|
1016
|
+
|
|
1017
|
+
ensure(hosted.content(), [
|
|
1018
|
+
ensure.one.html('form'),
|
|
1019
|
+
ensure.two.html('input').with.one.prop('name'),
|
|
1020
|
+
ensure.count(2).text('Bob'),
|
|
1021
|
+
ensure.no.html('video'),
|
|
1022
|
+
])
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
**The model is sightings, one flat list, a kind each.** The tree is flattened in document order into
|
|
1026
|
+
sightings: text, html elements by tag, components by the name stylesheets match, fragments, an
|
|
1027
|
+
element's classes one at a time, its id, and every prop but `children`. A fact meets only sightings
|
|
1028
|
+
of its own kind, and its quantifier counts the ones that *match* — so
|
|
1029
|
+
`no.html('a').with.one.prop('href')` says "no `a` with an href", not "no `a`". Depth does not
|
|
1030
|
+
matter: the walk goes through html elements, fragments and a component's children as written, so a
|
|
1031
|
+
fact at the top finds an element at any level.
|
|
1032
|
+
Text is joined as the page shows it — `Hi, {name}.` is one sighting, a number is text, a fragment
|
|
1033
|
+
does not break a run, an element either side of one does.
|
|
1034
|
+
|
|
1035
|
+
**Two things the walk does not enter.** A prop other than `children`: an element in
|
|
1036
|
+
`rows={[<tr/>]}` is a `prop:rows` sighting and nothing more until a fact opts in with `.content()`.
|
|
1037
|
+
And a component's rendered output where the tree does not hold it — the tree is the one given, so
|
|
1038
|
+
in a tree as written a component is read by name and by prop, not by what it renders. `host()`'s
|
|
1039
|
+
`content()` resolves by default, holding both (with `{ resolve: false }`, the tree as written); the
|
|
1040
|
+
facts are the same either way.
|
|
1041
|
+
|
|
1042
|
+
**Quantifiers**: `no` / `none`, `one` (`a` and `an` read the same), `two`, `three`, `count(n)`,
|
|
1043
|
+
`some` (one or more), `all` (every sighting of the kind matches, and there is at least one — all
|
|
1044
|
+
of none proves nothing). Each is on `ensure`, as `ensure.one`; the bare forms, for facts written
|
|
1045
|
+
inside a scope, come from the deep import:
|
|
1046
|
+
`import { one, two, children } from 'tungsten/testing/ensure.js'`.
|
|
1047
|
+
|
|
1048
|
+
**Ends** — what a sighting has to be:
|
|
1049
|
+
|
|
1050
|
+
- `text('Bob')`, `text.contains('ob')`, `text.matches(/^B/)`, and `textCi` for the same three
|
|
1051
|
+
ignoring case. `g` and `y` flags are dropped, so one regex is safe across sightings.
|
|
1052
|
+
- `interruptedText('BobAlice')` — an element's whole text with the markup between the words
|
|
1053
|
+
ignored, as `textContent` reads it; `.contains` and `.matches` too.
|
|
1054
|
+
- `html('button')`, `component('Badge')`, `fragment()`.
|
|
1055
|
+
- `select('nav > a.active')` — the elements a CSS selector matches (0.7.1+), counted by any
|
|
1056
|
+
quantifier and scoped like `html()`: `all.select('.bar').with.one.prop('data-t')`. Names are
|
|
1057
|
+
case-sensitive, and a capitalised one is a component, by the name `component()` matches, and it
|
|
1058
|
+
is the parent of what it holds: `select('Link > a')`, not `select('nav > a')`. Attributes are
|
|
1059
|
+
the props: `class` reads `className`, `for` reads `htmlFor`, `true` is present, `false`, `null`
|
|
1060
|
+
and `undefined` absent; a component's `className` and `id` are no class or id. `:has()`,
|
|
1061
|
+
`:not()` and the rest work. A scope is a tree of its own, so inside `.with.children` a selector
|
|
1062
|
+
sees no ancestor outside it. `select` is not taken by `with`: a match's own tag, classes and
|
|
1063
|
+
attributes go in its selector (`select('nav.main:has(a)')`).
|
|
1064
|
+
- `className('card')`, `id('page')` — one class at a time; with no argument, that it has one.
|
|
1065
|
+
- `prop('href')` — the prop is there, whatever its value, `undefined` included. Then `.is(value)`
|
|
1066
|
+
(`===`, so a handler or ref matches by reference), `.contains(text)`, `.matches(regex)`,
|
|
1067
|
+
`.exact(value)` (deep equal), `.subset(shape)` (extra keys and array elements pass, each shape
|
|
1068
|
+
element matching some element in any order), `.duck(shape)` (extra keys pass, but an array is a
|
|
1069
|
+
tuple: same length, in place), and `.content()` (the value is content; given facts, its tree).
|
|
1070
|
+
|
|
1071
|
+
**Scopes tie facts to one element.** Two facts in the list are independent —
|
|
1072
|
+
`one.html('button').with.one.prop('disabled')` and
|
|
1073
|
+
`one.html('button').with.children.one.text('Save')` can be met by two different buttons. To say one
|
|
1074
|
+
button is both, put both facts in the same scope:
|
|
1075
|
+
|
|
1076
|
+
- `.with([…])`, or chained as `.with.one.prop('name')` — properties of the match itself: `prop`,
|
|
1077
|
+
`className`, `id`, `interruptedText`, and `children` facts. `html`, `component`, `select`,
|
|
1078
|
+
`fragment`, `text` and `textCi` are not properties, so `with` does not take them (a type error;
|
|
1079
|
+
0.7.1+) — what the element holds is `.with.children`.
|
|
1080
|
+
`.with(props => boolean)` is the escape hatch for what facts cannot say, and the function's name
|
|
1081
|
+
is what the failure shows.
|
|
1082
|
+
- `.with.children([…])`, or `.with.children.one.text('Save')` — facts on the content it holds.
|
|
1083
|
+
- `children([…])` and `children.one.html('li')` — the same children facts, written for a `with`
|
|
1084
|
+
list, so they mix with the element's own facts and all of them apply to that one element:
|
|
1085
|
+
|
|
1086
|
+
```ts
|
|
1087
|
+
ensure(tree, [
|
|
1088
|
+
ensure.one.html('button').with([
|
|
1089
|
+
one.prop('disabled').is(true),
|
|
1090
|
+
children.one.text('Save'),
|
|
1091
|
+
]),
|
|
1092
|
+
])
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
A failure states the fact as prose, with the fact as written on a `rule:` line, and lists the
|
|
1096
|
+
sightings of its kind with the reason each one missed, down through the scopes. A reason names
|
|
1097
|
+
every inner fact the sighting missed, then those it met (`met: …`), and the nearest misses are
|
|
1098
|
+
listed first, ahead of the cap of 10. Where stdout has colour, sightings are white, misses red and
|
|
1099
|
+
facts met green; `{ color: false }` turns it off (0.7.1+):
|
|
1100
|
+
|
|
1101
|
+
```
|
|
1102
|
+
ensure: expected one <a> with one href="/y", saw 0
|
|
1103
|
+
rule: one.html('a').with.one.prop('href').is('/y')
|
|
1104
|
+
·
|
|
1105
|
+
<a> seen:
|
|
1106
|
+
<a href='/x'> (one href="/y": saw 0)
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
`ensure(tree, facts, { readable: false })` states it only as the fact was written:
|
|
1110
|
+
|
|
1111
|
+
```
|
|
1112
|
+
ensure: expected one.html('a').with.one.prop('href').is('/y'), saw 0
|
|
1113
|
+
html:a seen:
|
|
1114
|
+
<a href='/x'> (one.prop('href').is('/y'): saw 0)
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
### `tungsten compile` — preparing code for tests
|
|
1118
|
+
|
|
1119
|
+
A one-shot dev compilation without a server. By default it compiles JavaScript (JSX and the Babel
|
|
1120
|
+
transforms), stylesheets, client support files, assets and public files into `.tungsten/dev`,
|
|
1121
|
+
cleaning each selected target's existing output first (`--no-clean` keeps it). `--output <root>`
|
|
1122
|
+
selects an independent output root with the same layout (`code`, `.client`, `.static`, `.assets`,
|
|
1123
|
+
`.public`, `.componentCss`, `.packages`); cleaning and the package cache are scoped to it. Limit
|
|
1124
|
+
the work with `--js`, `--css`, `--client`, `--assets`, `--public` (`--all` is the explicit
|
|
1125
|
+
default). `--watch` keeps the js, css, asset and public compilers running; client support files
|
|
1126
|
+
are generated once. For orchestration, `--ready-file <file>` writes a small JSON file only after
|
|
1127
|
+
the initial compilation succeeds (removed by a failed watch cycle, recreated by the next success),
|
|
1128
|
+
and `--quiet` drops per-file messages but keeps errors and cycle status:
|
|
1129
|
+
|
|
1130
|
+
```sh
|
|
1131
|
+
tungsten compile --watch --output .tungsten/test --ready-file .tungsten/test.ready --quiet
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
Test files are left out of every compile unless `--tests` is passed: mark them in config with
|
|
1135
|
+
`testDirs` / `testPatterns` (the same two shapes as `ignoreDirs` / `ignorePatterns`, which drop
|
|
1136
|
+
files from every pipeline; `--ignore-dirs` / `--ignore-patterns` override those two from the
|
|
1137
|
+
command line). **On upgrade, add `--tests` to the test compile**, or it emits no test files and
|
|
1138
|
+
the runner finds nothing to run. Whether that fails CI is the runner's call: sxy-test-runner 2.4.12
|
|
1139
|
+
exits 3 for "no test files", earlier versions used other codes, and a runner that treats it as a
|
|
1140
|
+
pass goes green having run nothing, so check for the "no tests" case rather than assume. `--packages` also compiles imported packages into the build's `.packages` tree for
|
|
1141
|
+
the dev server to serve; it is off by default, and a use for it outside the dev server has not
|
|
1142
|
+
come up, since compiled code left to node's own resolution needs none of it. Which sources each
|
|
1143
|
+
pipeline compiles is set by `clientDirs` / `clientPatterns` (compiled for the browser with every
|
|
1144
|
+
plugin, into `.tungsten/<mode>/code`) and `rawDirs` / `rawPatterns` (compiled for node with types
|
|
1145
|
+
stripped and imports untouched, into `.tungsten/<mode>-raw`, for server middleware and the like).
|
|
1146
|
+
Each pipeline compiles **its selection and not the ignores**: client and not ignore, raw and not
|
|
1147
|
+
ignore. Unset, the client selection is everything and the raw selection is nothing; a file may be
|
|
1148
|
+
in both. Because the ignores win, ignoring server code also removes it from the raw pipeline: that
|
|
1149
|
+
is fine when the app's own build emits the server into `codeDir` (e.g. `ignoreDirs: ['server']`),
|
|
1150
|
+
but to have Tungsten compile it, narrow `clientDirs` and put it in `rawDirs` instead (e.g.
|
|
1151
|
+
`clientDirs: ['client', 'common'], rawDirs: ['server', 'common']`). Keep node-only modules out
|
|
1152
|
+
of the client set: the client pipeline does not tolerate imports of node built-ins when it
|
|
1153
|
+
compiles packages, which the dev server does. `--fresh` ignores the packages and styles caches.
|
|
1154
|
+
`sourceMaps: true` in `tungsten.config.js` appends an inline source map to each
|
|
1155
|
+
compiled app file in both pipelines, for `tungsten compile` and the dev server alike, chained
|
|
1156
|
+
through any map the source already references (e.g. tsc's `.js.map`), so debuggers (and node run
|
|
1157
|
+
with `--enable-source-maps`) point at the original source. Packages in `.packages` and the
|
|
1158
|
+
production bundle are not mapped. `tungsten compile --source-maps` / `--no-source-maps` override
|
|
1159
|
+
the config entry for one compile.
|
|
1160
|
+
|
|
1161
|
+
`skipPackageCompilation` in `tungsten.config.js` (one glob or an array, matched against the
|
|
1162
|
+
JavaScript files Tungsten receives, extensions untranslated) names files to transform and emit
|
|
1163
|
+
without traversing or copying their package dependencies; bare package imports in them are left as
|
|
1164
|
+
they are. Example: `'**/*.domTest.js'`.
|
|
1165
|
+
|
|
1166
|
+
## Dev server behaviour
|
|
1167
|
+
|
|
1168
|
+
Things that look like application bugs but are not:
|
|
1169
|
+
|
|
1170
|
+
- **HMR is on by default.** A source edit hot-swaps the changed component in place, keeping its
|
|
1171
|
+
position, props and state, and swaps CSS without a reload; anything it cannot handle falls back
|
|
1172
|
+
to a full page reload. The `hmr` config option picks the branch: `true` (default), `'reload'` for
|
|
1173
|
+
a plain full reload on every change, `false` for no live reload at all. Design and limits are in
|
|
1174
|
+
`docs/hmr.md`.
|
|
1175
|
+
- **Server modules are re-imported per request.** The dev server busts Node's ESM cache with an
|
|
1176
|
+
incrementing query string so server changes are picked up without a restart. Two consequences:
|
|
1177
|
+
- **Module-scope singletons are not singletons.** A database client or connection pool created
|
|
1178
|
+
at module scope in your server code is rebuilt on nearly every request, and old pools are not
|
|
1179
|
+
released — under load this exhausts the pool. The framework-sanctioned fix is the
|
|
1180
|
+
**`._cached_` filename convention**: the loader (`dist/moduleReloader.js`) exempts any module
|
|
1181
|
+
matching `*._cached_.{js,ts,jsx,tsx,…}` **and its dependency graph** from reloading. Put the
|
|
1182
|
+
client construction in e.g. `server/db/client._cached_.ts` and re-export it.
|
|
1183
|
+
- **A flat per-request latency** before any endpoint code runs, which grows with the number of
|
|
1184
|
+
first-party server modules, because the entry module's whole import graph is re-imported. It
|
|
1185
|
+
is the same for a database-free endpoint and for a 404. Do not chase it in application code;
|
|
1186
|
+
it does not exist in a production build.
|
|
1187
|
+
- Tungsten's own env globals *do* reload per page request, but a plain static import inside a
|
|
1188
|
+
middleware module resolves to the cached instance — so app-level env read at module scope by
|
|
1189
|
+
your own middleware still needs a restart.
|
|
1190
|
+
- **Startup fails loudly if the port is not fully yours** (0.6.0). Both servers
|
|
1191
|
+
reject on a bind error, and after binding fetch their own boot id from `/__tungsten_boot_probe__`
|
|
1192
|
+
over `127.0.0.1` and `::1`; if another process answers on either, or accepts and never answers,
|
|
1193
|
+
`tungsten dev`/`start` exits non-zero naming the address and the reason, instead of printing
|
|
1194
|
+
"ready" while a stale server takes the traffic. Stop the other server and start again.
|
|
1195
|
+
- **`TUNGSTEN_SERVER_PORT=0` starts dev on a free port** (0.6.0). The ready line
|
|
1196
|
+
names the port taken, and the browser opens on it. Pairing a random port with a fixed
|
|
1197
|
+
`FRONTEND_PORT` warns at startup: a proxy cannot be pointed at a port that changes every boot.
|
|
1198
|
+
- **The origin-mismatch warning compares hosts, not whole origins** (0.6.0). It
|
|
1199
|
+
fires when a request carries an `Origin` matching your configured frontend but arrives at a
|
|
1200
|
+
different host — the case where cookies silently do not travel. The scheme is ignored on purpose:
|
|
1201
|
+
Tungsten always serves http, so behind a TLS-terminating proxy every request would otherwise look
|
|
1202
|
+
like a mismatch, which is what earlier versions reported. `suppressOriginMismatchWarning: true`
|
|
1203
|
+
turns it off; the production server repeats it at most once a day per host.
|
|
1204
|
+
|
|
1205
|
+
## Everyday workflow
|
|
1206
|
+
|
|
1207
|
+
1. Add or adjust the API endpoint and its tests.
|
|
1208
|
+
2. Add the route and its `data` loader for the page's initial read.
|
|
1209
|
+
3. Build the page as a typed class component; use the store wrapper only if it needs shared state.
|
|
1210
|
+
4. Bind ordinary form fields; call the API from event handlers for mutations.
|
|
1211
|
+
5. Handle every result status the endpoint can return, then refresh local or route data.
|
|
1212
|
+
6. Type-check, then run the test suite.
|
|
1213
|
+
|
|
1214
|
+
## Things to avoid
|
|
1215
|
+
|
|
1216
|
+
The principle behind most of these: **do not deviate between the server render and the client
|
|
1217
|
+
render.** In the normal flow the two are identical, and hydration depends on it. The only sanctioned
|
|
1218
|
+
deviations are updates after the first render, `DocInit()` mutations made before hydration, and
|
|
1219
|
+
client-only rendering — `csr: true` in config (the body ships empty and the client mounts fresh),
|
|
1220
|
+
`serverSideRouting: false` (the head's children too, and the client fetches the data), or a
|
|
1221
|
+
component's `static render = 'client'` / `static render = { location: 'client', placeholder }`
|
|
1222
|
+
(the server emits the placeholder, the client renders the component). Anything else that makes
|
|
1223
|
+
the first client render differ from the server's markup is a bug, not a technique.
|
|
1224
|
+
|
|
1225
|
+
- Do not use `setState`, immutable state updates, or React hooks. Mutate state directly.
|
|
1226
|
+
- Do not mutate state inside `content` or anything it calls.
|
|
1227
|
+
- Do not write a descendant selector for markup a **child component** renders — name the
|
|
1228
|
+
component as the selector instead. Check this every time you style what turns out to be a
|
|
1229
|
+
component.
|
|
1230
|
+
- Do not rely on tag-vs-class specificity in scoped styles; they are equal.
|
|
1231
|
+
- Do not fetch a page's initial data only in `clientInit` — it loses SSR and the first render.
|
|
1232
|
+
- Do not touch browser globals in `init`, route code, or `content` without a guard; use
|
|
1233
|
+
`clientInit`.
|
|
1234
|
+
- Do not produce environment-dependent output during render — it breaks hydration.
|
|
1235
|
+
- Do not bypass `<Link>` for a path that should stay client-side.
|
|
1236
|
+
- Do not put page-local state, or fetched records, in the global store to avoid passing props.
|
|
1237
|
+
- Do not set `title` on anything but a page component.
|
|
1238
|
+
- Do not create module-scope connections or caches in server code without the `._cached_`
|
|
1239
|
+
convention.
|
|
1240
|
+
- Do not put event handlers in `<head>` — it is not hydrated.
|
|
1241
|
+
- Do not use `dangerouslySetInnerHTML` for inline `<script>`/`<style>` code — their children are
|
|
1242
|
+
not escaped; write `<script>{code}</script>`. And do not expect an attribute a boot script sets on
|
|
1243
|
+
`<html>`/`<body>` to survive hydration unless the root component renders it too.
|
|
1244
|
+
- Do not "correct" `.jsx`/`.js` import extensions in `.tsx` source.
|
|
1245
|
+
- Do not treat the published documentation's names and examples as definitive over the installed
|
|
1246
|
+
declarations.
|