├── .github └── workflows │ └── release.yml ├── .gitignore ├── .indo.json ├── .npmrc ├── .prettierignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE.md ├── README.md ├── bundleTypes.js ├── examples └── react-basic │ ├── README.md │ ├── data │ └── pokemon.json │ ├── package.json │ ├── public │ ├── bulbasaur.webp │ ├── charmander.webp │ ├── favicon.svg │ ├── logo.png │ ├── pikachu.webp │ ├── pokemon.png │ └── squirtle.webp │ ├── src │ ├── App.css │ ├── App.tsx │ ├── Router.tsx │ ├── components │ │ └── Link.tsx │ ├── layouts │ │ └── default.tsx │ ├── node │ │ ├── connect.ts │ │ ├── html.ts │ │ ├── routes.ts │ │ ├── server.ts │ │ └── tsconfig.json │ ├── routes │ │ ├── Home.tsx │ │ ├── NotFound.tsx │ │ ├── Pokemon.css │ │ └── Pokemon.tsx │ ├── state.ts │ ├── tsconfig.json │ └── url.ts │ ├── tsconfig.json │ └── vite.config.ts ├── index.d.ts ├── package.json ├── packages ├── aws │ ├── cloudfront │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ ├── request.ts │ │ │ │ └── types.ts │ │ │ ├── createInvalidation.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── s3-website │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── config.ts │ │ │ ├── deploy.ts │ │ │ ├── index.ts │ │ │ ├── runtime │ │ │ │ ├── emptyPageStore.ts │ │ │ │ ├── index.ts │ │ │ │ ├── purgePageStore.ts │ │ │ │ ├── setupPageStore.ts │ │ │ │ └── types.ts │ │ │ ├── secrets.ts │ │ │ ├── sync.ts │ │ │ ├── types.ts │ │ │ └── varyByDevice.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── s3 │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── api │ │ │ │ ├── headers.ts │ │ │ │ ├── params.ts │ │ │ │ ├── request.ts │ │ │ │ └── types.ts │ │ │ ├── copyObject.ts │ │ │ ├── deleteObjects.ts │ │ │ ├── emptyBucket.ts │ │ │ ├── index.ts │ │ │ ├── listObjects.ts │ │ │ ├── moveObjects.ts │ │ │ ├── putObject.ts │ │ │ ├── store.ts │ │ │ └── utils │ │ │ │ ├── pick.ts │ │ │ │ ├── throttle.ts │ │ │ │ └── types.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ └── utils │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── src │ │ ├── index.ts │ │ ├── request.ts │ │ ├── response.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── xml.ts │ │ └── xml │ │ │ ├── parse.ts │ │ │ └── unescape.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts ├── cloudflare │ ├── cache │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── src │ │ │ ├── index.ssr.ts │ │ │ ├── index.ts │ │ │ ├── purgeAllFiles.ts │ │ │ └── ssr │ │ │ │ └── purge.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── dns │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── src │ │ │ ├── hook.ts │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── page-rules │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── src │ │ │ ├── hook.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ └── request │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.d.ts │ │ ├── package.json │ │ ├── pnpm-lock.yaml │ │ ├── src │ │ ├── index.ts │ │ ├── request.ts │ │ └── secrets.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts ├── cloudform │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── api │ │ │ ├── describeStack.ts │ │ │ ├── describeStackEvents.ts │ │ │ ├── request.ts │ │ │ └── types.ts │ │ ├── hook.ts │ │ ├── index.ts │ │ ├── secrets.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── cloudimage │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── src │ │ ├── config.ts │ │ ├── defaults.ts │ │ ├── hook.ts │ │ ├── index.ts │ │ ├── secrets.ts │ │ ├── snakeCase.ts │ │ └── types │ │ │ ├── login.ts │ │ │ ├── payload.ts │ │ │ └── session.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── git-push │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── src │ │ ├── config.ts │ │ ├── index.ts │ │ ├── init.ts │ │ ├── push-hook.ts │ │ ├── push.ts │ │ └── stash.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── page-store │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── purge │ │ │ └── plugin.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── react │ ├── LICENSE │ ├── README.md │ ├── client │ │ ├── hydrate.ts │ │ └── tsconfig.json │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── index.ts │ │ ├── plugin.ts │ │ ├── stack.tsx │ │ └── types.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── types │ │ ├── index.d.ts │ │ └── plugin.d.ts │ └── vite.config.ts ├── secrets │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── git.ts │ │ ├── index.ssr.ts │ │ ├── index.ts │ │ └── unsafe.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── vercel │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── functions │ │ │ ├── hook.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.ts └── webp │ ├── LICENSE │ ├── README.md │ ├── index.d.ts │ ├── package.json │ ├── src │ └── convert.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── patches ├── @rollup__pluginutils@5.0.1.patch ├── es-module-lexer@0.9.3.patch └── quick-lru@6.1.1.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── spec └── build.test.ts ├── src ├── .eslintrc.js ├── README.md ├── build │ ├── api.ts │ ├── failedPages.ts │ ├── multicast.ts │ ├── pageFactory.ts │ ├── runBundle.ts │ └── worker.ts ├── bundle │ ├── api.ts │ ├── clientPreloads.ts │ ├── clients.ts │ ├── context.ts │ ├── html.ts │ ├── html │ │ ├── inject.ts │ │ ├── serialize.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── isolateRoutes.spec.ts │ ├── isolateRoutes.ts │ ├── liveBindings.ts │ ├── moduleRedirects.ts │ ├── options.ts │ ├── preferExternal.ts │ ├── renderBundleModule.ts │ ├── routeImports.ts │ ├── routes │ │ ├── appVersion.ts │ │ ├── clientStore.ts │ │ └── clientStorePlugin.ts │ ├── runtime │ │ ├── README.md │ │ ├── api.ts │ │ ├── bundle │ │ │ ├── api.ts │ │ │ ├── app.ts │ │ │ ├── clientAssets.ts │ │ │ ├── clientEntries.ts │ │ │ ├── clientModules.ts │ │ │ ├── clientStore │ │ │ │ ├── index.ts │ │ │ │ ├── inline.ts │ │ │ │ └── local.ts │ │ │ ├── clientStyles.ts │ │ │ ├── config.ts │ │ │ ├── context.ts │ │ │ ├── debug.ts │ │ │ ├── debugBase.ts │ │ │ ├── injectSausClient.ts │ │ │ ├── pageBundles.ts │ │ │ ├── paths.ts │ │ │ ├── render.ts │ │ │ ├── routes.ts │ │ │ ├── server │ │ │ │ ├── connect.ts │ │ │ │ ├── debug.ts │ │ │ │ ├── fileCache.ts │ │ │ │ ├── index.ts │ │ │ │ ├── serveCachedFiles.ts │ │ │ │ ├── servePages.ts │ │ │ │ └── servePublicDir.ts │ │ │ └── writePages.ts │ │ ├── client │ │ │ ├── api.ts │ │ │ └── pageClient.ts │ │ ├── core │ │ │ ├── api.ts │ │ │ └── constants.ts │ │ └── defineSecrets.ts │ ├── runtimeBundle.ts │ ├── ssrBundle.ts │ └── types.ts ├── cli.ts ├── cli │ ├── actions │ │ ├── build.ts │ │ ├── bundle.ts │ │ ├── deploy │ │ │ ├── default.ts │ │ │ ├── index.ts │ │ │ └── sync.ts │ │ ├── dev.ts │ │ ├── index.ts │ │ ├── preview.ts │ │ ├── secrets │ │ │ ├── add.ts │ │ │ ├── index.ts │ │ │ ├── ls.ts │ │ │ ├── rm.ts │ │ │ └── set.ts │ │ └── test.ts │ └── command.ts ├── client │ ├── README.md │ ├── api.ts │ ├── baseUrl.ts │ ├── context.ts │ ├── defineLayout.ts │ ├── dist │ │ ├── baseUrl.cjs │ │ ├── index.cjs │ │ ├── index.d.ts │ │ ├── node │ │ │ └── context.cjs │ │ ├── package.json │ │ └── routes.cjs │ ├── dynamicImport.ts │ ├── head.ts │ ├── helpers.ts │ ├── http │ │ └── get.ts │ ├── hydrate.ts │ ├── importState.ts │ ├── index.dev.ts │ ├── index.prod.ts │ ├── index.ts │ ├── isDebug.ts │ ├── loadPageState.ts │ ├── node │ │ ├── README.md │ │ ├── api.ts │ │ ├── loadPageState.ts │ │ └── pageClient.ts │ ├── package.json │ ├── pageClient.ts │ ├── preCacheState.ts │ ├── preloadModules.ts │ ├── prependBase.ts │ ├── renderErrorPage.ts │ ├── renderPage.ts │ ├── routes.ts │ ├── stateModules │ │ ├── get.ts │ │ ├── global.ts │ │ ├── hydrate.ts │ │ ├── serve.ts │ │ └── setState.ts │ ├── textDecoder.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── core │ ├── AssetStore.ts │ ├── api.ts │ ├── babel │ │ ├── exports.ts │ │ ├── imports.ts │ │ ├── package.json │ │ └── resolveReferences.ts │ ├── cache.ts │ ├── compileRoutesMap.ts │ ├── context.ts │ ├── core.ts │ ├── debug.ts │ ├── defineClient.ts │ ├── findConfigFiles.ts │ ├── getBundleHash.ts │ ├── getEntryModules.ts │ ├── getRequireFunctions.ts │ ├── getSausPlugins.ts │ ├── git.ts │ ├── index.ts │ ├── injectModules.ts │ ├── loadBundle.ts │ ├── loadRoutes.ts │ ├── moduleRedirects.ts │ ├── package.json │ ├── paths.ts │ ├── plugins │ │ ├── __snapshots__ │ │ │ └── clientState.spec.ts.snap │ │ ├── clientContext.ts │ │ ├── clientLayout.ts │ │ ├── clientState.spec.ts │ │ ├── clientState.ts │ │ ├── debug.ts │ │ ├── httpImport.ts │ │ ├── moduleProvider.ts │ │ ├── moduleRedirection.ts │ │ ├── publicDir.ts │ │ ├── routeClients.ts │ │ ├── routes.ts │ │ ├── serve.ts │ │ └── ssrLayout.ts │ ├── profiling.ts │ ├── publicDir.ts │ ├── rollup.ts │ ├── routeClients.ts │ ├── routeEntries.ts │ ├── routeRenderer.ts │ ├── setEnvData.ts │ ├── testPlugin.ts │ ├── virtualRoutes.ts │ ├── vite.ts │ ├── vite │ │ ├── checkPublicFile.ts │ │ ├── collectCss.ts │ │ ├── compileNodeModule.ts │ │ ├── compileSsrModule.ts │ │ ├── config.ts │ │ ├── configDeps.ts │ │ ├── configFile.ts │ │ ├── esbuildPlugin.ts │ │ ├── functions.ts │ │ ├── modulePreload.ts │ │ ├── requireHook.ts │ │ ├── resolveEntryUrl.ts │ │ └── upsertPlugin.ts │ └── writeBundle.ts ├── deploy │ ├── api.ts │ ├── bump.ts │ ├── context.ts │ ├── debug.ts │ ├── files.ts │ ├── helpers.ts │ ├── hooks.ts │ ├── index.ts │ ├── loader.ts │ ├── logger.ts │ ├── options.ts │ ├── pluginCache.ts │ ├── polling.ts │ ├── prepareBundle.ts │ ├── revert.ts │ ├── sync.ts │ ├── targetCache.ts │ ├── types.ts │ └── utils.ts ├── dev │ ├── api.ts │ ├── context.ts │ ├── createDevApp.ts │ ├── events.ts │ └── hotReload.ts ├── dist │ ├── bin │ │ ├── browserslist │ │ └── saus │ ├── bundle │ │ ├── index.d.ts │ │ └── index.js │ ├── core │ │ └── index.d.ts │ ├── deploy │ │ └── index.d.ts │ ├── env │ │ ├── client.d.ts │ │ └── node.d.ts │ ├── index.d.ts │ ├── package.json │ └── vite │ │ ├── index.d.ts │ │ ├── index.js │ │ └── index.mjs ├── index.ts ├── package.json ├── preview │ ├── api.ts │ └── options.ts ├── purge │ ├── index.ts │ ├── onDeploy.ts │ ├── purgeServerCache.ts │ ├── request.ts │ ├── route.ts │ └── types.ts ├── runtime │ ├── README.md │ ├── app │ │ ├── cacheClientProps.ts │ │ ├── cachePages.ts │ │ ├── collectStateFiles.ts │ │ ├── constants.ts │ │ ├── createApp.ts │ │ ├── errorFallback.ts │ │ ├── index.ts │ │ ├── internal │ │ │ ├── builtinRoutes.ts │ │ │ ├── endpoints.ts │ │ │ ├── loadPageProps.ts │ │ │ ├── renderPage.ts │ │ │ ├── renderPageState.ts │ │ │ └── renderStateModule.ts │ │ ├── logRequests.ts │ │ ├── negotiator.ts │ │ ├── throttleRender.ts │ │ └── types.ts │ ├── bundleTypes.ts │ ├── cache │ │ ├── access.ts │ │ ├── clear.ts │ │ ├── context.ts │ │ ├── create.ts │ │ ├── expiration.ts │ │ ├── forEach.ts │ │ ├── global.ts │ │ └── types.ts │ ├── cachePlugin.ts │ ├── clientHooks.ts │ ├── clientTypes.ts │ ├── config.ts │ ├── constants.ts │ ├── dataToEsm.spec.ts │ ├── dataToEsm.ts │ ├── debug.ts │ ├── deployedEnv.ts │ ├── dist │ │ ├── app │ │ │ ├── errorFallback.css │ │ │ └── errorFallbackClient.js │ │ └── package.json │ ├── emptyModule.ts │ ├── endpoint.ts │ ├── endpointHooks.ts │ ├── getLayoutEntry.ts │ ├── getLoadedStateOrThrow.ts │ ├── getPagePath.ts │ ├── getStateModuleKey.ts │ ├── global.ts │ ├── html │ │ ├── __snapshots__ │ │ │ └── parser.spec.ts.snap │ │ ├── debug.ts │ │ ├── download.ts │ │ ├── index.ts │ │ ├── minify.ts │ │ ├── onChange.ts │ │ ├── parser.spec.ts │ │ ├── parser.ts │ │ ├── path.ts │ │ ├── process.ts │ │ ├── resolver.ts │ │ ├── selector.spec.ts │ │ ├── selector.ts │ │ ├── symbols.ts │ │ ├── template.ts │ │ ├── test.ts │ │ ├── traversal.ts │ │ ├── types.ts │ │ ├── visitors.spec.ts │ │ ├── visitors │ │ │ ├── bind.ts │ │ │ └── merge.ts │ │ ├── xss.ts │ │ └── xss │ │ │ └── allow.ts │ ├── http │ │ ├── cacheKey.ts │ │ ├── debug.ts │ │ ├── get.ts │ │ ├── headers.ts │ │ ├── hooks.ts │ │ ├── http.ts │ │ ├── httpImport.ts │ │ ├── index.ts │ │ ├── internal │ │ │ ├── startRequest.ts │ │ │ └── urlToHttpOptions.ts │ │ ├── jsonImport.ts │ │ ├── normalizeHeaders.ts │ │ ├── redirect.ts │ │ ├── response.ts │ │ ├── responseCache.ts │ │ ├── types.ts │ │ ├── unwrapBody.ts │ │ ├── wrapBody.ts │ │ ├── writeBody.ts │ │ ├── writeHeaders.ts │ │ └── writeResponse.ts │ ├── imports.ts │ ├── includeState.ts │ ├── layoutRenderer.ts │ ├── layouts.ts │ ├── makeRequest.ts │ ├── mapStateModule.ts │ ├── package.json │ ├── parseRoutePath.ts │ ├── renderHtml.ts │ ├── renderPageScript.ts │ ├── renderRoutePath.ts │ ├── renderer.ts │ ├── requestMetadata.ts │ ├── routeHooks.ts │ ├── routePlugins.ts │ ├── routeTypes.ts │ ├── routes │ │ ├── generateRoutePaths.ts │ │ └── matchRoute.ts │ ├── setLayout.ts │ ├── setup.ts │ ├── ssrModules.ts │ ├── stateModules.ts │ ├── stateModules │ │ ├── README.md │ │ ├── debug.ts │ │ ├── get.ts │ │ ├── global.ts │ │ ├── hydrate.ts │ │ ├── serve.ts │ │ └── setState.ts │ ├── tokens.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── url.ts ├── secrets │ ├── api.ts │ ├── defineSecrets.ts │ ├── hub.ts │ ├── loadSecretSources.ts │ ├── plugin.ts │ ├── prompt.ts │ ├── runtime │ │ ├── addSecrets.ts │ │ ├── checkSecrets.ts │ │ └── index.ts │ ├── symbols.ts │ ├── types.ts │ └── utils │ │ └── selectSource.ts ├── tsconfig.json ├── tsup.config.ts ├── utils │ ├── AbortController │ │ ├── index.node.ts │ │ ├── index.ts │ │ └── types.ts │ ├── LazyPromise.ts │ ├── __snapshots__ │ │ └── esmToCjs.spec.ts.snap │ ├── array.ts │ ├── ascendBranch.ts │ ├── assignDefaults.ts │ ├── babel.ts │ ├── babel │ │ ├── config.ts │ │ ├── program.ts │ │ ├── queries.ts │ │ └── transform.ts │ ├── base.ts │ ├── buffer.ts │ ├── callPlugins.ts │ ├── cleanUrl.ts │ ├── combineSourcemaps.ts │ ├── controlExecution.ts │ ├── dedupe.ts │ ├── defer.ts │ ├── defineLazy.ts │ ├── diffObjects.ts │ ├── dist │ │ └── package.json │ ├── escape.ts │ ├── generateId.ts │ ├── getPageFilename.ts │ ├── httpMethods.ts │ ├── importRegex.ts │ ├── isCSSRequest.ts │ ├── isExternalUrl.ts │ ├── isObject.ts │ ├── isPackageRef.ts │ ├── joinUrl.ts │ ├── keys.ts │ ├── klona.ts │ ├── limitTime.ts │ ├── magic-string.ts │ ├── mapSerial.ts │ ├── memoizeFn.ts │ ├── murmur3.ts │ ├── node │ │ ├── ansiToHtml.ts │ │ ├── bindExec.ts │ │ ├── buffer.ts │ │ ├── compileCache.ts │ │ ├── compileCache │ │ │ └── fileMappings.ts │ │ ├── currentModule.ts │ │ ├── emptyDir.ts │ │ ├── findPackage.ts │ │ ├── getRawGitHubUrl.ts │ │ ├── git │ │ │ └── createCommit.ts │ │ ├── lazyImport.ts │ │ ├── limitConcurrency.ts │ │ ├── printFiles.ts │ │ ├── prompt.ts │ │ ├── relativeToCwd.ts │ │ ├── servedPathForFile.ts │ │ ├── shortcuts.ts │ │ ├── sourceMap.ts │ │ ├── stack │ │ │ ├── getStackFrame.ts │ │ │ ├── index.ts │ │ │ ├── resolveStackTrace.ts │ │ │ └── traceStackFrame.ts │ │ ├── textDecoder.ts │ │ ├── tinypool.ts │ │ └── toDebugPath.ts │ ├── noop.ts │ ├── objectHash.ts │ ├── package.json │ ├── parseHead.ts │ ├── parseLazyImport.ts │ ├── parseStackTrace.ts │ ├── pick.ts │ ├── plural.ts │ ├── readJson.ts │ ├── reduceSerial.ts │ ├── resolveModules.ts │ ├── rollupTypes.ts │ ├── sortObjects.ts │ ├── streamToBuffer.ts │ ├── stripHtmlSuffix.spec.ts │ ├── stripHtmlSuffix.ts │ ├── take.ts │ ├── textExtensions.ts │ ├── throttle.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── types.ts │ └── unwrapDefault.ts └── vm │ ├── ImporterSet.ts │ ├── asyncRequire.ts │ ├── compileEsm.spec.ts │ ├── compileEsm.ts │ ├── compileModule.ts │ ├── debug.ts │ ├── dedupeNodeResolve.ts │ ├── dist │ └── package.json │ ├── esmInterop.ts │ ├── executeModule.ts │ ├── exportNotFound.ts │ ├── forceNodeReload.ts │ ├── formatAsyncStack.ts │ ├── fullReload.ts │ ├── hookNodeResolve.ts │ ├── index.ts │ ├── isLiveModule.ts │ ├── moduleMap.ts │ ├── nodeModules.ts │ ├── overwriteScript.ts │ ├── package.json │ ├── traceNodeRequire.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── types.ts ├── test ├── config.ts ├── context.ts └── index.ts └── vitest.config.ts /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | 9 | - uses: actions/setup-node@v1 10 | with: 11 | node-version: ${{ matrix.node_version }} 12 | 13 | - name: Install 14 | uses: pnpm/action-setup@v1.2.1 15 | with: 16 | version: 5 17 | run_install: | 18 | args: [--frozen-lockfile] 19 | 20 | - name: Test 21 | run: yarn test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/imagetools 2 | packages/repng 3 | packages/test 4 | node_modules 5 | starters 6 | vendor 7 | dist 8 | !dist/package.json 9 | .vscode/pinned-files.json 10 | .tsbuildinfo 11 | -------------------------------------------------------------------------------- /.indo.json: -------------------------------------------------------------------------------- 1 | { 2 | "repos": { 3 | "packages/imagetools": { 4 | "url": "https://github.com/alloc/saus-imagetools.git", 5 | "optional": true 6 | }, 7 | "packages/repng": { 8 | "url": "https://github.com/alloc/saus-repng.git", 9 | "optional": true 10 | }, 11 | "packages/test": { 12 | "url": "https://github.com/alloc/saus-test.git", 13 | "optional": true 14 | }, 15 | "vendor/regexparam": { 16 | "url": "https://github.com/aleclarson/regexparam.git" 17 | }, 18 | "vendor/navaid": { 19 | "url": "https://github.com/aleclarson/navaid.git" 20 | }, 21 | "vendor/cac": { 22 | "url": "https://github.com/aleclarson/cac" 23 | } 24 | }, 25 | "alias": { 26 | "vite": "@alloc/vite" 27 | } 28 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-workspace-root-check = true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src/core/api.ts -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["kbysiec.vscode-search-everywhere", "pmneo.tsimporter"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": ["source.organizeImports"], 3 | "files.exclude": { 4 | "**/.git": true, 5 | "**/dist": true 6 | }, 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "searchEverywhere.shouldDisplayNotificationInStatusBar": true, 9 | "searchEverywhere.exclude": [ 10 | "**/.git", 11 | "**/.DS_Store", 12 | "**/pnpm-lock.yaml", 13 | "**/node_modules/**", 14 | "**/vendor/**", 15 | "**/dist/**" 16 | ], 17 | "tsimporter.emitSemicolon": false, 18 | "tsimporter.filesToExclude": ["**/vendor/**", "**/dist/**"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/react-basic/data/pokemon.json: -------------------------------------------------------------------------------- 1 | ["Pikachu", "Bulbasaur", "Charmander", "Squirtle"] 2 | -------------------------------------------------------------------------------- /examples/react-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-basic", 3 | "private": true, 4 | "scripts": { 5 | "dev": "saus dev", 6 | "build": "saus build", 7 | "serve": "cd dist && node $SERVE_OPTIONS node/server.mjs" 8 | }, 9 | "dependencies": { 10 | "@saus/html": "*", 11 | "@saus/react": "*", 12 | "@saus/test": "*", 13 | "@saus/webp": "*", 14 | "@types/node": "^16.10.2", 15 | "misty": "^1.6.0", 16 | "navaid": "^1.2.0", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "saus": "*" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/react-basic/public/bulbasaur.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/examples/react-basic/public/bulbasaur.webp -------------------------------------------------------------------------------- /examples/react-basic/public/charmander.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/examples/react-basic/public/charmander.webp -------------------------------------------------------------------------------- /examples/react-basic/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/examples/react-basic/public/logo.png -------------------------------------------------------------------------------- /examples/react-basic/public/pikachu.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/examples/react-basic/public/pikachu.webp -------------------------------------------------------------------------------- /examples/react-basic/public/pokemon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/examples/react-basic/public/pokemon.png -------------------------------------------------------------------------------- /examples/react-basic/public/squirtle.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/examples/react-basic/public/squirtle.webp -------------------------------------------------------------------------------- /examples/react-basic/src/App.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 16px; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 4 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 5 | } 6 | 7 | div { 8 | display: flex; 9 | flex-direction: column; 10 | } 11 | 12 | a { 13 | padding: 0.2rem; 14 | } 15 | 16 | header { 17 | width: 100%; 18 | height: 6rem; 19 | border-bottom: 1px solid hsl(0, 0%, 92%); 20 | margin-bottom: 1.25rem; 21 | display: flex; 22 | align-items: center; 23 | justify-content: center; 24 | } 25 | 26 | header img { 27 | height: 66%; 28 | } 29 | 30 | main { 31 | padding: 0 2rem; 32 | } 33 | 34 | h1 { 35 | font-size: 2.6rem; 36 | font-weight: 600; 37 | margin-bottom: 2rem; 38 | } 39 | -------------------------------------------------------------------------------- /examples/react-basic/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Router } from './Router' 2 | import logoPng from '/logo.png' 3 | import './App.css' 4 | 5 | export function App(props: { children: JSX.Element }) { 6 | return ( 7 | <> 8 |
9 | 10 |
11 |
12 | {props.children} 13 |
14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /examples/react-basic/src/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react' 2 | import { BASE_URL } from 'saus/client' 3 | 4 | export type LinkProps = ComponentProps<'a'> & { href: string } 5 | 6 | export const Link = ({ href, ...props }: LinkProps) => ( 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /examples/react-basic/src/node/html.ts: -------------------------------------------------------------------------------- 1 | import { downloadRemoteAssets } from '@saus/html' 2 | import { MistyTask, startTask } from 'misty/task' 3 | import { success } from 'misty' 4 | 5 | export default (options: { cacheAssets: boolean }) => { 6 | if (options.cacheAssets) { 7 | const tasks = new Map() 8 | downloadRemoteAssets({ 9 | onRequest: url => { 10 | tasks.set(url, startTask(`Downloading: ${url}`)) 11 | }, 12 | onResponse: url => { 13 | tasks.get(url)!.finish() 14 | tasks.delete(url) 15 | }, 16 | onWriteFile: file => { 17 | success(`Saved asset: ${file}`) 18 | }, 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/react-basic/src/node/routes.ts: -------------------------------------------------------------------------------- 1 | import { route } from 'saus' 2 | import pokemonList from '../../data/pokemon.json' 3 | import { scrapedText } from '../state' 4 | import configureHtml from './html' 5 | 6 | route('/', () => import('../routes/Home')) 7 | 8 | route('/pokemon/:name', () => import('../routes/Pokemon'), { 9 | paths: () => pokemonList.map(name => name.toLowerCase()), 10 | include: ({ name }) => [scrapedText.bind(name)], 11 | }) 12 | 13 | route(() => import('../routes/NotFound')) 14 | 15 | configureHtml({ 16 | cacheAssets: false, 17 | }) 18 | -------------------------------------------------------------------------------- /examples/react-basic/src/node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "jsx": "react", 6 | "resolveJsonModule": true, 7 | "types": ["node"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/react-basic/src/routes/Home.tsx: -------------------------------------------------------------------------------- 1 | import pokemon from '../../data/pokemon.json' 2 | import { Link } from '../components/Link' 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 |

Home

8 |
9 | {pokemon.map((name, i) => ( 10 | 11 | {name} 12 | 13 | ))} 14 | 404 Test 15 |
16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /examples/react-basic/src/routes/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '../components/Link' 2 | 3 | export default () => ( 4 | <> 5 |

Page not found

6 | Go back 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /examples/react-basic/src/routes/Pokemon.css: -------------------------------------------------------------------------------- 1 | .pokemon .content { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: flex-start; 5 | align-self: center; 6 | margin-left: -10em; 7 | } 8 | 9 | .pokemon h2 { 10 | margin-bottom: 0.6em; 11 | } 12 | 13 | .pokemon .sections { 14 | font-family: 'Noto Serif'; 15 | font-size: 1.8em; 16 | max-width: 800px; 17 | margin-left: 1em; 18 | margin-bottom: 8em; 19 | } 20 | 21 | .pokemon img { 22 | width: 18em; 23 | } 24 | 25 | .pokemon p { 26 | font-size: 0.8em; 27 | line-height: 1.6em; 28 | } 29 | -------------------------------------------------------------------------------- /examples/react-basic/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "jsx": "react-jsx", 6 | "lib": ["dom", "esnext"], 7 | "resolveJsonModule": true, 8 | "types": ["saus/env/client"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/react-basic/src/url.ts: -------------------------------------------------------------------------------- 1 | export function prependBase(id: string) { 2 | return import.meta.env.BASE_URL + id.replace(/^\//, '') 3 | } 4 | -------------------------------------------------------------------------------- /examples/react-basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["esnext"], 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "noEmit": true, 7 | "noUnusedLocals": true, 8 | "sourceMap": true, 9 | "strict": true, 10 | "target": "esnext", 11 | "types": ["@saus/react"] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-basic/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { convertToWebp } from '@saus/webp' 2 | import { defineConfig } from 'saus' 3 | 4 | export default defineConfig({ 5 | base: '/staging/', 6 | saus: { 7 | routes: './src/node/routes.ts', 8 | bundle: { 9 | target: 'node16', 10 | entry: './src/node/server.ts', 11 | format: 'esm', 12 | debugBase: '/debug/', 13 | // minify: true, 14 | }, 15 | renderConcurrency: 1, 16 | }, 17 | build: { 18 | // minify: false, 19 | }, 20 | plugins: [convertToWebp()], 21 | }) 22 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/aws/cloudfront/.gitignore: -------------------------------------------------------------------------------- 1 | test.js 2 | -------------------------------------------------------------------------------- /packages/aws/cloudfront/README.md: -------------------------------------------------------------------------------- 1 | # @saus/aws-cloudfront 2 | -------------------------------------------------------------------------------- /packages/aws/cloudfront/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/aws/cloudfront/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/aws-cloudfront", 3 | "version": "0.1.0", 4 | "description": "Manage your Amazon CloudFront distributions", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "saus": "*" 23 | }, 24 | "dependencies": { 25 | "@saus/aws-utils": "workspace:^0.1.0", 26 | "mrmime": "^1.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/aws/cloudfront/src/api/request.ts: -------------------------------------------------------------------------------- 1 | import { createAmzRequestFn } from '@saus/aws-utils' 2 | import { CloudFront } from './types' 3 | 4 | export const signedRequest = (region: string) => 5 | createAmzRequestFn<{ 6 | CreateInvalidation: { 7 | params: CloudFront.CreateInvalidationRequest 8 | result: CloudFront.CreateInvalidationResult 9 | } 10 | }>({ 11 | region, 12 | service: 'cloudfront', 13 | apiVersion: '2020-05-31', 14 | globalSubdomain: true, 15 | }) 16 | -------------------------------------------------------------------------------- /packages/aws/cloudfront/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createInvalidation' 2 | -------------------------------------------------------------------------------- /packages/aws/cloudfront/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/aws/cloudfront/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | clean: true, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/aws/s3-website/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | export * from './src/runtime' 3 | -------------------------------------------------------------------------------- /packages/aws/s3-website/src/index.ts: -------------------------------------------------------------------------------- 1 | export { deployWebsiteToS3 } from './deploy' 2 | export { emptyPageStore, purgePageStore } from './runtime' 3 | -------------------------------------------------------------------------------- /packages/aws/s3-website/src/runtime/index.ts: -------------------------------------------------------------------------------- 1 | export * from './emptyPageStore' 2 | export * from './purgePageStore' 3 | export * from './setupPageStore' 4 | -------------------------------------------------------------------------------- /packages/aws/s3-website/src/runtime/purgePageStore.ts: -------------------------------------------------------------------------------- 1 | import { PurgePlugin } from 'saus' 2 | import { emptyPageStore } from './emptyPageStore' 3 | import { PurgeProps } from './types' 4 | 5 | export function purgePageStore(props: PurgeProps): PurgePlugin { 6 | return { 7 | name: '@aws/s3-website:purgePageStore', 8 | purge(request) { 9 | if (request.globs.has('/*')) { 10 | return emptyPageStore(props) 11 | } 12 | // TODO: purge specific pages! 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/aws/s3-website/src/runtime/setupPageStore.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from '@saus/aws-s3' 2 | import { PageStoreConfig as Config, setupPageStore as setup } from '@saus/page-store' 3 | 4 | export type PageStoreConfig = Omit & { 5 | bucket: string 6 | region: string 7 | } 8 | 9 | export function setupPageStore(config: PageStoreConfig) { 10 | setup({ 11 | store: createStore(config.bucket, config.region), 12 | ...config, 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /packages/aws/s3-website/src/runtime/types.ts: -------------------------------------------------------------------------------- 1 | export interface PurgeProps { 2 | region: string 3 | /** 4 | * Pass the app version to avoid duplicate cache invalidations 5 | * for the same deployment (in case of deployment errors). 6 | */ 7 | appVersion?: string 8 | /** 9 | * CloudFront distribution ID 10 | * 11 | * When defined, the CloudFront cache is invalidated. 12 | */ 13 | cacheId?: string 14 | /** 15 | * S3 bucket name 16 | * 17 | * When defined, the bucket is emptied. 18 | */ 19 | bucket?: string 20 | } 21 | -------------------------------------------------------------------------------- /packages/aws/s3-website/src/secrets.ts: -------------------------------------------------------------------------------- 1 | import { defineSecrets } from 'saus' 2 | 3 | export default defineSecrets({ 4 | accessKeyId: 'AWS_ACCESS_KEY_ID', 5 | secretAccessKey: 'AWS_SECRET_ACCESS_KEY', 6 | }) 7 | -------------------------------------------------------------------------------- /packages/aws/s3-website/src/varyByDevice.ts: -------------------------------------------------------------------------------- 1 | export type UserDeviceType = keyof typeof deviceHeaders 2 | 3 | const deviceHeaders = { 4 | android: 'CloudFront-Is-Android-Viewer', 5 | desktop: 'CloudFront-Is-Desktop-Viewer', 6 | ios: 'CloudFront-Is-IOS-Viewer', 7 | mobile: 'CloudFront-Is-Mobile-Viewer', 8 | tablet: 'CloudFront-Is-Tablet-Viewer', 9 | tv: 'CloudFront-Is-SmartTV-Viewer', 10 | } as const 11 | 12 | export function varyByDevice( 13 | deviceTypes: readonly UserDeviceType[] | undefined 14 | ): string[] { 15 | if (deviceTypes?.length) { 16 | return deviceTypes.map( 17 | (type): string => deviceHeaders[type as keyof typeof deviceHeaders] 18 | ) 19 | } 20 | return [] 21 | } 22 | -------------------------------------------------------------------------------- /packages/aws/s3-website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/aws/s3-website/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['src/index.ts'], 6 | format: ['cjs', 'esm'], 7 | }, 8 | { 9 | entry: { 'index.ssr': 'src/runtime/index.ts' }, 10 | format: ['esm'], 11 | }, 12 | ]) 13 | -------------------------------------------------------------------------------- /packages/aws/s3/.gitignore: -------------------------------------------------------------------------------- 1 | test.js 2 | -------------------------------------------------------------------------------- /packages/aws/s3/README.md: -------------------------------------------------------------------------------- 1 | # @saus/aws-s3 2 | -------------------------------------------------------------------------------- /packages/aws/s3/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | export * from './src/index.ssr' 3 | -------------------------------------------------------------------------------- /packages/aws/s3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/aws-s3", 3 | "version": "0.1.0", 4 | "description": "Manage your Amazon S3 buckets", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "exports": { 9 | "types": "./index.d.ts", 10 | "import": "./dist/index.mjs", 11 | "default": "./dist/index.js" 12 | }, 13 | "files": [ 14 | "dist", 15 | "index.d.ts" 16 | ], 17 | "scripts": { 18 | "build": "tsup", 19 | "dev": "tsup --watch --sourcemap", 20 | "prepare": "yarn build" 21 | }, 22 | "peerDependencies": { 23 | "saus": "*" 24 | }, 25 | "devDependencies": { 26 | "saus": "*" 27 | }, 28 | "dependencies": { 29 | "@saus/aws-utils": "workspace:^0.1.0", 30 | "mrmime": "^1.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/aws/s3/src/api/params.ts: -------------------------------------------------------------------------------- 1 | export const commonParamKeys = ['Action', 'Version', 'Bucket'] as const 2 | -------------------------------------------------------------------------------- /packages/aws/s3/src/api/request.ts: -------------------------------------------------------------------------------- 1 | import { createAmzRequestFn } from '@saus/aws-utils' 2 | import { S3 } from './types' 3 | 4 | export const signedRequest = (region: string) => 5 | createAmzRequestFn<{ 6 | PutObject: { 7 | params: S3.PutObjectRequest 8 | result: S3.PutObjectOutput 9 | } 10 | ListObjects: { 11 | params: S3.ListObjectsV2Request 12 | result: S3.ListObjectsV2Output 13 | } 14 | DeleteObjects: { 15 | params: S3.DeleteObjectsRequest 16 | result: S3.DeleteObjectsOutput 17 | } 18 | CopyObject: { 19 | params: S3.CopyObjectRequest 20 | result: S3.CopyObjectResult 21 | } 22 | }>({ 23 | region, 24 | service: 's3', 25 | apiVersion: '2006-03-01', 26 | }) 27 | -------------------------------------------------------------------------------- /packages/aws/s3/src/copyObject.ts: -------------------------------------------------------------------------------- 1 | import { parseXmlResponse } from '@saus/aws-utils' 2 | import { controlExecution } from 'saus/utils/controlExecution' 3 | import { paramsToHeaders } from './api/headers' 4 | import { signedRequest } from './api/request' 5 | import { writeThrottler } from './utils/throttle' 6 | 7 | export function copyObject(region: string) { 8 | return controlExecution( 9 | signedRequest(region).action('CopyObject', { 10 | coerceRequest: params => ({ 11 | method: 'put', 12 | subdomain: params.Bucket, 13 | path: params.Key, 14 | query: null, 15 | headers: paramsToHeaders(params, ['Key']), 16 | }), 17 | coerceResponse(resp) { 18 | return parseXmlResponse(resp).CopyObjectResult 19 | }, 20 | }) 21 | ).with(writeThrottler) 22 | } 23 | -------------------------------------------------------------------------------- /packages/aws/s3/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './copyObject' 2 | export * from './deleteObjects' 3 | export * from './emptyBucket' 4 | export * from './listObjects' 5 | export * from './moveObjects' 6 | export * from './putObject' 7 | export * from './store' 8 | -------------------------------------------------------------------------------- /packages/aws/s3/src/utils/pick.ts: -------------------------------------------------------------------------------- 1 | export function pick>( 2 | obj: T, 3 | keys: P, 4 | filter: (value: any, key: P[number]) => boolean = () => true 5 | ): Pick { 6 | const picked: any = {} 7 | for (const key of keys) { 8 | const value = obj[key] 9 | if (filter(value, key)) { 10 | picked[key] = value 11 | } 12 | } 13 | return picked 14 | } 15 | 16 | export function pickAllExcept< 17 | T extends object, 18 | P extends ReadonlyArray 19 | >(obj: T, keys: P): Pick> 20 | 21 | export function pickAllExcept( 22 | obj: Record, 23 | keys: readonly string[] 24 | ): Record 25 | 26 | export function pickAllExcept(obj: any, keys: readonly (keyof any)[]) { 27 | return pick(obj, Object.keys(obj) as any, (_, key) => !keys.includes(key)) 28 | } 29 | -------------------------------------------------------------------------------- /packages/aws/s3/src/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionGate } from 'saus/utils/controlExecution' 2 | 3 | // https://aws.amazon.com/premiumsupport/knowledge-center/s3-request-limit-avoid-throttling/ 4 | const maxWritesPerSecond = 3500 5 | 6 | let writes = 0 7 | let writesPerSecond = 0 8 | let timeout: NodeJS.Timeout | null = null 9 | 10 | export const writeThrottler: ExecutionGate = async (ctx, args) => { 11 | if (writesPerSecond == maxWritesPerSecond) { 12 | ctx.queuedCalls.push(args) 13 | } else { 14 | writes++ 15 | writesPerSecond++ 16 | timeout ||= setTimeout(() => { 17 | writesPerSecond = writes 18 | }, 1000) 19 | 20 | await ctx.execute(args) 21 | writes-- 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/aws/s3/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { CamelCase } from 'type-fest' 2 | 3 | export type CamelCasedPropertiesDeep = Value extends Function 4 | ? Value 5 | : Value extends Array 6 | ? Array> 7 | : Value extends Set 8 | ? Set> 9 | : Value extends { [Symbol.iterator]: any } | { [Symbol.toPrimitive]: any } 10 | ? Value 11 | : { 12 | [K in keyof Value as CamelCase]: CamelCasedPropertiesDeep 13 | } 14 | -------------------------------------------------------------------------------- /packages/aws/s3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/aws/s3/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | clean: true, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/aws/utils/README.md: -------------------------------------------------------------------------------- 1 | # @saus/aws-utils 2 | -------------------------------------------------------------------------------- /packages/aws/utils/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/aws/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/aws-utils", 3 | "version": "0.1.0", 4 | "description": "Common functions for AWS-specific Saus packages", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "@types/aws4": "^1.11.2", 23 | "saus": "*" 24 | }, 25 | "dependencies": { 26 | "aws4": "^1.11.0", 27 | "type-fest": "^2.13.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/aws/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request' 2 | export * from './response' 3 | export * from './xml' 4 | -------------------------------------------------------------------------------- /packages/aws/utils/src/types.ts: -------------------------------------------------------------------------------- 1 | import { CamelCase } from 'type-fest' 2 | 3 | export type CamelCasedPropertiesDeep = Value extends Function 4 | ? Value 5 | : Value extends Array 6 | ? Array> 7 | : Value extends Set 8 | ? Set> 9 | : Value extends { [Symbol.iterator]: any } | { [Symbol.toPrimitive]: any } 10 | ? Value 11 | : { 12 | [K in keyof Value as CamelCase]: CamelCasedPropertiesDeep 13 | } 14 | -------------------------------------------------------------------------------- /packages/aws/utils/src/utils.ts: -------------------------------------------------------------------------------- 1 | // fooBar -> FooBar 2 | export function pascalize(key: string) { 3 | return key[0].toUpperCase() + key.slice(1) 4 | } 5 | 6 | // FooBar -> fooBar 7 | export function camelize(key: string) { 8 | return key[0].toLowerCase() + key.slice(1) 9 | } 10 | -------------------------------------------------------------------------------- /packages/aws/utils/src/xml/unescape.ts: -------------------------------------------------------------------------------- 1 | export function unescape(input: string) { 2 | return input 3 | .replace(/>/g, '>') 4 | .replace(/</g, '<') 5 | .replace(/�?39;/g, "'") 6 | .replace(/"/g, '"') 7 | .replace(/&/g, '&') // Must happen last or else it will unescape other characters in the wrong order. 8 | } 9 | -------------------------------------------------------------------------------- /packages/aws/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/aws/utils/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | clean: true, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/cloudflare/cache/README.md: -------------------------------------------------------------------------------- 1 | # @saus/cloudflare-cache 2 | -------------------------------------------------------------------------------- /packages/cloudflare/cache/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/cloudflare/cache/src/index.ssr.ts: -------------------------------------------------------------------------------- 1 | export * from './ssr/purge' 2 | -------------------------------------------------------------------------------- /packages/cloudflare/cache/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './purgeAllFiles' 2 | export * from './ssr/purge' 3 | -------------------------------------------------------------------------------- /packages/cloudflare/cache/src/purgeAllFiles.ts: -------------------------------------------------------------------------------- 1 | import { createRequestFn, secrets } from '@saus/cloudflare-request' 2 | import { addSecrets, getDeployContext } from 'saus/deploy' 3 | 4 | addSecrets(purgeAllFiles, secrets) 5 | 6 | /** 7 | * Purge the Cloudflare cache of all files. 8 | * 9 | * Call this within an `onDeploy` callback, and make sure 10 | * to check `ctx.dryRun` before doing so. 11 | */ 12 | export function purgeAllFiles(zoneId: string) { 13 | const ctx = getDeployContext() 14 | const request = createRequestFn({ 15 | apiToken: secrets.apiToken, 16 | logger: ctx.logger, 17 | }) 18 | return request('post', `/zones/${zoneId}/purge_cache`, { 19 | body: { json: { purge_everything: true } }, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /packages/cloudflare/cache/src/ssr/purge.ts: -------------------------------------------------------------------------------- 1 | import { createRequestFn, secrets } from '@saus/cloudflare-request' 2 | import { PurgePlugin } from 'saus' 3 | 4 | export function purgeCloudflare(zoneId: string): PurgePlugin { 5 | return { 6 | name: '@saus/cloudflare-cache:purge', 7 | async purge(request) { 8 | if (request.globs.has('/*')) { 9 | const request = createRequestFn({ 10 | apiToken: secrets.apiToken, 11 | logger: { info: console.log }, 12 | }) 13 | await request('post', `/zones/${zoneId}/purge_cache`, { 14 | body: { json: { purge_everything: true } }, 15 | }) 16 | } else { 17 | // TODO: path-specific purging 18 | } 19 | }, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/cloudflare/cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cloudflare/cache/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['src/index.ts'], 6 | format: ['cjs', 'esm'], 7 | }, 8 | { 9 | entry: ['src/index.ssr.ts'], 10 | format: ['esm'], 11 | }, 12 | ]) 13 | -------------------------------------------------------------------------------- /packages/cloudflare/dns/README.md: -------------------------------------------------------------------------------- 1 | # @saus/cloudflare-dns 2 | -------------------------------------------------------------------------------- /packages/cloudflare/dns/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/cloudflare/dns/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/cloudflare-dns", 3 | "version": "0.1.0", 4 | "description": "Manage DNS records with Cloudflare", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "saus": "*" 23 | }, 24 | "dependencies": { 25 | "@saus/cloudflare-request": "^0.1.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cloudflare/dns/src/index.ts: -------------------------------------------------------------------------------- 1 | import { secrets } from '@saus/cloudflare-request' 2 | import { addDeployHook, addDeployTarget, addSecrets } from 'saus/deploy' 3 | import { DnsRecordList } from './types' 4 | 5 | const hook = addDeployHook(() => import('./hook.js')) 6 | addSecrets(useCloudflareDNS, secrets) 7 | 8 | export function useCloudflareDNS(zoneId: string, records: DnsRecordList) { 9 | for (const rec of records) { 10 | rec.ttl ??= 1 11 | } 12 | return addDeployTarget(hook, { 13 | zoneId, 14 | records, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /packages/cloudflare/dns/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function toTable( 2 | list: readonly T[], 3 | identify: (item: T) => Id 4 | ): Record 5 | 6 | export function toTable( 7 | list: readonly T[], 8 | identify: (item: T) => Id, 9 | unwrap: (item: T) => U 10 | ): Record 11 | 12 | export function toTable( 13 | list: readonly any[], 14 | identify: (item: any) => string | number, 15 | unwrap: (item: any) => any = item => item 16 | ) { 17 | const table: any = {} 18 | list.forEach(item => { 19 | const id = identify(item) 20 | table[id] = unwrap(item) 21 | }) 22 | return table 23 | } 24 | -------------------------------------------------------------------------------- /packages/cloudflare/dns/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cloudflare/dns/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/hook.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: true, 7 | clean: true, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/cloudflare/page-rules/README.md: -------------------------------------------------------------------------------- 1 | # @saus/cloudflare-page-rules 2 | -------------------------------------------------------------------------------- /packages/cloudflare/page-rules/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/cloudflare/page-rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/cloudflare-page-rules", 3 | "version": "0.1.0", 4 | "description": "Manage page rules on Cloudflare", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "saus": "*" 23 | }, 24 | "dependencies": {} 25 | } 26 | -------------------------------------------------------------------------------- /packages/cloudflare/page-rules/src/hook.ts: -------------------------------------------------------------------------------- 1 | // import { defineDeployHook } from 'saus/deploy' 2 | 3 | // export default defineDeployHook(async ctx => { 4 | // return { 5 | // name: 'cloudflare-page-rules', 6 | // } 7 | // }) 8 | -------------------------------------------------------------------------------- /packages/cloudflare/page-rules/src/index.ts: -------------------------------------------------------------------------------- 1 | // import { addDeployHook, addDeployTarget, addSecrets } from 'saus/deploy' 2 | // import secrets from './secrets' 3 | 4 | // const hook = addDeployHook(() => import('./hook')) 5 | // addSecrets(useCloudflarePageRules, secrets) 6 | 7 | // export function useCloudflarePageRules() { 8 | // return addDeployTarget(hook, {}) 9 | // } 10 | -------------------------------------------------------------------------------- /packages/cloudflare/page-rules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cloudflare/page-rules/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/hook.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: true, 7 | clean: true, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/cloudflare/request/README.md: -------------------------------------------------------------------------------- 1 | # @saus/cloudflare-request 2 | 3 | ```ts 4 | import { createRequestFn } from '@saus/cloudflare-request' 5 | 6 | const request = createRequestFn({ 7 | // Get a token here: https://dash.cloudflare.com/profile/api-tokens 8 | apiToken: '**********', 9 | // Usually a Vite logger is passed here. 10 | logger: { info: console.log }, 11 | }) 12 | 13 | const resp = await request('get', '/zones/cd7d0123e3012345da9420df9514dad0') 14 | console.log(resp.toJSON()) 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/cloudflare/request/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/cloudflare/request/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/cloudflare-request", 3 | "version": "0.1.0", 4 | "description": "Make requests to Cloudflare v4 API", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "saus": "*" 23 | }, 24 | "dependencies": {} 25 | } 26 | -------------------------------------------------------------------------------- /packages/cloudflare/request/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request' 2 | export * from './secrets' 3 | -------------------------------------------------------------------------------- /packages/cloudflare/request/src/secrets.ts: -------------------------------------------------------------------------------- 1 | import { defineSecrets } from 'saus' 2 | 3 | export const secrets = defineSecrets({ 4 | apiToken: 'CLOUDFLARE_API_TOKEN', 5 | }) 6 | -------------------------------------------------------------------------------- /packages/cloudflare/request/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cloudflare/request/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: true, 7 | clean: true, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/cloudform/README.md: -------------------------------------------------------------------------------- 1 | # @saus/cloudform 2 | -------------------------------------------------------------------------------- /packages/cloudform/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | export * from './src/types' 3 | -------------------------------------------------------------------------------- /packages/cloudform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/cloudform", 3 | "version": "0.1.0", 4 | "description": "Manage your AWS infra with CloudFormation", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "recrawl-sync": "^2.2.1", 23 | "saus": "*" 24 | }, 25 | "dependencies": { 26 | "@saus/aws-utils": "workspace:^0.1.0", 27 | "cloudform-types": "npm:@alloc/cloudform-types@^7.4.3", 28 | "debug": "^4.3.2", 29 | "dset": "^3.1.2", 30 | "type-fest": "^2.13.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/cloudform/src/api/describeStackEvents.ts: -------------------------------------------------------------------------------- 1 | import secrets from '../secrets' 2 | import { Stack } from '../types' 3 | import { signedRequest } from './request' 4 | 5 | export async function describeStackEvents(stack: Stack) { 6 | if (!stack.id) { 7 | throw Error('Expected stack.id to exist') 8 | } 9 | const getEvents = signedRequest.action('DescribeStackEvents', { 10 | region: stack.region, 11 | creds: secrets, 12 | }) 13 | const { stackEvents } = await getEvents({ 14 | stackName: stack.id, 15 | }) 16 | return stackEvents || [] 17 | } 18 | -------------------------------------------------------------------------------- /packages/cloudform/src/secrets.ts: -------------------------------------------------------------------------------- 1 | import { defineSecrets } from 'saus' 2 | 3 | export default defineSecrets({ 4 | accessKeyId: 'AWS_ACCESS_KEY_ID', 5 | secretAccessKey: 'AWS_SECRET_ACCESS_KEY', 6 | }) 7 | -------------------------------------------------------------------------------- /packages/cloudform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cloudform/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/hook.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: true, 7 | clean: true, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/cloudimage/README.md: -------------------------------------------------------------------------------- 1 | # @saus/cloudimage 2 | -------------------------------------------------------------------------------- /packages/cloudimage/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/cloudimage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/cloudimage", 3 | "version": "0.1.0", 4 | "description": "Manage your Cloudimage settings", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "@types/lodash.merge": "^4.6.7", 23 | "saus": "*" 24 | }, 25 | "dependencies": { 26 | "lodash.merge": "^4.6.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/cloudimage/src/index.ts: -------------------------------------------------------------------------------- 1 | import { addDeployHook, addDeployTarget, addSecrets } from 'saus/deploy' 2 | import { Config } from './config' 3 | import secrets from './secrets' 4 | 5 | const hook = addDeployHook(() => import('./hook.js')) 6 | addSecrets(useCloudimage, secrets) 7 | 8 | export function useCloudimage(config: Config) { 9 | return addDeployTarget(hook, config) 10 | } 11 | -------------------------------------------------------------------------------- /packages/cloudimage/src/secrets.ts: -------------------------------------------------------------------------------- 1 | import { defineSecrets } from 'saus' 2 | 3 | export default defineSecrets({ 4 | email: 'CLOUDIMAGE_EMAIL', 5 | password: 'CLOUDIMAGE_PASSWORD', 6 | }) 7 | -------------------------------------------------------------------------------- /packages/cloudimage/src/snakeCase.ts: -------------------------------------------------------------------------------- 1 | export const snakeCase = (text: string) => 2 | text 3 | .replace(/^[A-Z]/, ch => ch.toLowerCase()) 4 | .replace(/[A-Z]+/g, text => '_' + text.toLowerCase()) 5 | -------------------------------------------------------------------------------- /packages/cloudimage/src/types/login.ts: -------------------------------------------------------------------------------- 1 | export interface LoginResponse { 2 | status: string 3 | session_uuid: string 4 | user_uuid: string 5 | user_email: string 6 | redirect_to: string 7 | companies: Company[] 8 | msg: string 9 | } 10 | 11 | export interface Company { 12 | id: string 13 | uuid: string 14 | slug: string 15 | name: string 16 | uniq: null 17 | } 18 | -------------------------------------------------------------------------------- /packages/cloudimage/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cloudimage/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/hook.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: true, 7 | clean: true, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/git-push/README.md: -------------------------------------------------------------------------------- 1 | # @saus/git-push 2 | 3 | Commit and push a git repository during deployment, with automatic rollback support. 4 | -------------------------------------------------------------------------------- /packages/git-push/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/git-push/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/git-push", 3 | "version": "0.1.0", 4 | "description": "Git-based deployment for Saus apps", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "@types/lodash.merge": "^4.6.7", 23 | "saus": "*" 24 | }, 25 | "dependencies": { 26 | "lodash.merge": "^4.6.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/git-push/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './init' 3 | export * from './push' 4 | -------------------------------------------------------------------------------- /packages/git-push/src/push.ts: -------------------------------------------------------------------------------- 1 | import { addDeployHook, addDeployTarget } from 'saus/deploy' 2 | import { PushConfig } from './config' 3 | 4 | const hook = addDeployHook(() => import('./push-hook.js')) 5 | 6 | /** 7 | * Push a local clone to its origin. 8 | */ 9 | export function gitPush(config: PushConfig) { 10 | return addDeployTarget(hook, config) 11 | } 12 | -------------------------------------------------------------------------------- /packages/git-push/src/stash.ts: -------------------------------------------------------------------------------- 1 | export const stashedRoots = new Set() 2 | -------------------------------------------------------------------------------- /packages/git-push/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/git-push/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/hook.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: true, 7 | clean: true, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/page-store/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/page-store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/page-store", 3 | "version": "0.1.0", 4 | "description": "Store rendered pages somewhere else", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist", 11 | "index.d.ts" 12 | ], 13 | "scripts": { 14 | "build": "tsup", 15 | "dev": "tsup --watch --sourcemap", 16 | "prepare": "yarn build" 17 | }, 18 | "peerDependencies": { 19 | "saus": "*" 20 | }, 21 | "devDependencies": { 22 | "saus": "*" 23 | }, 24 | "dependencies": {} 25 | } 26 | -------------------------------------------------------------------------------- /packages/page-store/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/page-store/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['cjs', 'esm'], 6 | clean: true, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/react/client/hydrate.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { hydrate } from 'react-dom' 3 | import { defineHydrator } from 'saus/client' 4 | 5 | export default defineHydrator((root, content) => { 6 | hydrate(content, root) 7 | }) 8 | -------------------------------------------------------------------------------- /packages/react/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["dom"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom/server' 2 | import { defineLayoutRenderer } from 'saus/core' 3 | import './stack' 4 | 5 | export const defineLayout = defineLayoutRenderer({ 6 | hydrator: '@saus/react/hydrator', 7 | toString: ReactDOM.renderToString, 8 | }) 9 | 10 | export * from './types' 11 | -------------------------------------------------------------------------------- /packages/react/src/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from 'react' 2 | import { BareRoute, CommonClientProps } from 'saus/core' 3 | 4 | type Remap = {} & { [P in keyof T]: T[P] } 5 | 6 | /** 7 | * Use this to statically type the `props` method of each route. 8 | * This assumes your route modules export a default component, 9 | * which expects the route props in its component props. 10 | */ 11 | export type RouteProps = unknown & 12 | T extends BareRoute 13 | ? Module extends { [Key in K]: ComponentType } 14 | ? Remap> 15 | : unknown 16 | : unknown 17 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "vite.config.ts", "hydrator.ts"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "lib": ["es2018"], 8 | "module": "commonjs", 9 | "moduleResolution": "node16", 10 | "noUnusedLocals": true, 11 | "outDir": "dist", 12 | "strict": true, 13 | "sourceMap": true, 14 | "target": "es2019" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/plugin.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: true, 7 | clean: true, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/react/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../src/index' 2 | -------------------------------------------------------------------------------- /packages/react/types/plugin.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../src/plugin' 2 | -------------------------------------------------------------------------------- /packages/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import reactVite from '@vitejs/plugin-react' 2 | import { defineConfig } from 'saus/core' 3 | 4 | export default defineConfig({ 5 | ssr: { 6 | external: ['react', 'react-dom'], 7 | }, 8 | optimizeDeps: { 9 | include: ['react', 'react-dom'], 10 | }, 11 | plugins: [reactVite()], 12 | }) 13 | -------------------------------------------------------------------------------- /packages/secrets/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/secrets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/secrets", 3 | "version": "0.1.0", 4 | "description": "Load password-encrypted secrets from git", 5 | "license": "MIT", 6 | "exports": { 7 | "types": "./index.d.ts", 8 | "ssr": "./dist/index.ssr.mjs", 9 | "import": "./dist/index.mjs", 10 | "default": "./dist/index.js" 11 | }, 12 | "files": [ 13 | "dist", 14 | "index.d.ts" 15 | ], 16 | "scripts": { 17 | "build": "tsup", 18 | "dev": "tsup --watch --sourcemap", 19 | "prepare": "yarn build" 20 | }, 21 | "peerDependencies": { 22 | "saus": "*" 23 | }, 24 | "devDependencies": { 25 | "saus": "*" 26 | }, 27 | "dependencies": { 28 | "@cush/exec": "^1.8.0", 29 | "aes-password": "^1.0.5" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/secrets/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './git' 2 | export * from './unsafe' 3 | 4 | /** 5 | * Read deployed secrets from your GitHub repository. \ 6 | * You need to call `useGitSecrets` in your deploy file for this 7 | * to work correctly. 8 | * 9 | * ⚠︎ Available in SSR bundle only. 10 | * 11 | * During development, this function resolves with an empty object. 12 | * 13 | * @param repoId - The repository ID, like "user/project" 14 | * @param authToken - For private repository access 15 | * @param password - Used to decrypt the secrets file 16 | */ 17 | export async function loadGitHubSecrets( 18 | repoId: string, 19 | authToken: string, 20 | password: string 21 | ) { 22 | return {} as Record 23 | } 24 | -------------------------------------------------------------------------------- /packages/secrets/src/unsafe.ts: -------------------------------------------------------------------------------- 1 | import { getDeployContext, SecretMap } from 'saus/deploy' 2 | 3 | export function setUnsafeSecrets(values: SecretMap) { 4 | const { secrets } = getDeployContext() 5 | secrets.set(values) 6 | } 7 | -------------------------------------------------------------------------------- /packages/secrets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/secrets/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig([ 4 | { 5 | entry: ['src/index.ts'], 6 | format: ['cjs', 'esm'], 7 | clean: true, 8 | }, 9 | { 10 | entry: ['src/index.ssr.ts'], 11 | format: ['esm'], 12 | clean: true, 13 | }, 14 | ]) 15 | -------------------------------------------------------------------------------- /packages/vercel/README.md: -------------------------------------------------------------------------------- 1 | # @saus/vercel 2 | -------------------------------------------------------------------------------- /packages/vercel/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/index' 2 | -------------------------------------------------------------------------------- /packages/vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/vercel", 3 | "version": "0.1.0", 4 | "description": "Deploy a Saus app to Vercel", 5 | "license": "MIT", 6 | "main": "dist/index.js", 7 | "module": "dist/index.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "tsup", 14 | "dev": "tsup --watch --sourcemap", 15 | "prepare": "yarn build" 16 | }, 17 | "peerDependencies": { 18 | "saus": "*" 19 | }, 20 | "devDependencies": { 21 | "saus": "*" 22 | }, 23 | "dependencies": { 24 | "@cush/exec": "^1.8.0", 25 | "recrawl-sync": "^2.2.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/vercel/src/functions/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { addDeployHook, addDeployTarget, getDeployContext } from 'saus/deploy' 3 | import { Props } from './types' 4 | 5 | const hook = addDeployHook(() => import('./hook')) 6 | 7 | export function pushVercelFunctions(options: Props) { 8 | const { root } = getDeployContext() 9 | const functionDir = path.resolve(root, options.functionDir) 10 | return addDeployTarget(hook, { 11 | gitBranch: options.gitBranch, 12 | functionDir: path.relative(root, functionDir), 13 | entries: options.entries, 14 | minify: options.minify, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /packages/vercel/src/functions/types.ts: -------------------------------------------------------------------------------- 1 | export interface Props { 2 | /** Relative globs and/or paths to the API functions. */ 3 | entries?: string[] 4 | /** All entries are relative to this directory. */ 5 | functionDir: string 6 | /** 7 | * The branch that Vercel is watching. \ 8 | * This must not be used by another deployment. 9 | */ 10 | gitBranch: string 11 | minify?: boolean 12 | } 13 | -------------------------------------------------------------------------------- /packages/vercel/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './functions' 2 | export * from './types' 3 | -------------------------------------------------------------------------------- /packages/vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": false, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/vercel/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/functions/hook.ts'], 5 | format: ['cjs', 'esm'], 6 | splitting: true, 7 | clean: true, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/webp/README.md: -------------------------------------------------------------------------------- 1 | # @saus/webp 2 | 3 | Convert images to `.webp` format 4 | -------------------------------------------------------------------------------- /packages/webp/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './src/convert' 2 | -------------------------------------------------------------------------------- /packages/webp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/webp", 3 | "version": "0.1.0", 4 | "description": "Convert images to WEBP format", 5 | "license": "MIT", 6 | "main": "dist/convert.js", 7 | "module": "dist/convert.mjs", 8 | "types": "index.d.ts", 9 | "files": [ 10 | "src", 11 | "dist", 12 | "index.d.ts" 13 | ], 14 | "scripts": { 15 | "build": "rimraf dist && tsup", 16 | "dev": "tsup --watch", 17 | "prepare": "yarn build" 18 | }, 19 | "dependencies": { 20 | "@rollup/pluginutils": "^4.1.2", 21 | "imagemin": "^7", 22 | "imagemin-webp": "^6", 23 | "kleur": "^4.1.4", 24 | "misty": "^1.6.2" 25 | }, 26 | "peerDependencies": { 27 | "saus": "*" 28 | }, 29 | "devDependencies": { 30 | "@types/imagemin-webp": "^7.0.0", 31 | "saus": "*" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/webp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "moduleResolution": "node16", 9 | "noUnusedLocals": true, 10 | "outDir": "dist", 11 | "strict": true, 12 | "sourceMap": true, 13 | "target": "es2019" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/webp/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/*'], 5 | format: ['cjs', 'esm'], 6 | bundle: false, 7 | sourcemap: true, 8 | }) 9 | -------------------------------------------------------------------------------- /patches/@rollup__pluginutils@5.0.1.patch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/patches/@rollup__pluginutils@5.0.1.patch -------------------------------------------------------------------------------- /patches/es-module-lexer@0.9.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index bdabf21f4040523b3f3abe632b272f80ad6e073d..3ca5b1e821ab23359c0245883ac2777d4132df3a 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -7,6 +7,7 @@ 6 | "types": "types/lexer.d.ts", 7 | "exports": { 8 | ".": { 9 | + "types": "./types/lexer.d.ts", 10 | "module": "./dist/lexer.js", 11 | "import": "./dist/lexer.js", 12 | "require": "./dist/lexer.cjs" -------------------------------------------------------------------------------- /patches/quick-lru@6.1.1.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 31e84c2f4fd234aa5186c2affd336b44f54414c8..7057120642d02589d3e87d9f0e17dd29ec763458 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -10,7 +10,6 @@ 6 | "email": "sindresorhus@gmail.com", 7 | "url": "https://sindresorhus.com" 8 | }, 9 | - "type": "module", 10 | "exports": "./index.js", 11 | "engines": { 12 | "node": ">=12" -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'src' 3 | - 'src/dist' 4 | - 'src/*' 5 | - 'packages/*' 6 | - 'packages/aws/*' 7 | - 'packages/cloudflare/*' 8 | - '!packages/repng' 9 | - '!packages/imagetools' 10 | - 'examples/*' 11 | -------------------------------------------------------------------------------- /spec/build.test.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/spec/build.test.ts -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: [ 6 | 'tsconfig.json', 7 | 'client/tsconfig.json', 8 | 'runtime/tsconfig.json', 9 | 'utils/tsconfig.json', 10 | 'vm/tsconfig.json', 11 | ], 12 | }, 13 | plugins: ['@typescript-eslint'], 14 | rules: { 15 | '@typescript-eslint/no-floating-promises': 'error', 16 | '@typescript-eslint/no-misused-promises': 'error', 17 | }, 18 | root: true, 19 | ignorePatterns: ['*.js'], 20 | } 21 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## Project Structure 2 | 3 | - `client/` \ 4 | The `saus/client` API lives here. \ 5 | Used in SSR and browser contexts. 6 | 7 | - `core/` \ 8 | Supporting modules for development, builds, and more. 9 | 10 | - `vite/` \ 11 | Assorted helpers with a dependency on Vite. \ 12 | Not specific to Saus internals. 13 | 14 | - `runtime/` \ 15 | Isomorphic internals of client and SSR runtimes. 16 | 17 | - `app/` \ 18 | The application layer lives here. Rendering and routing. \ 19 | Used by dev server and SSR bundles. 20 | 21 | - `html/` \ 22 | Reusable logic for HTML processing 23 | 24 | - `http/` \ 25 | Isomorphic HTTP helpers and types 26 | 27 | - `utils/` \ 28 | Isomorphic isolated helpers. 29 | 30 | - `node/` \ 31 | Assorted helpers for Node.js only 32 | -------------------------------------------------------------------------------- /src/build/failedPages.ts: -------------------------------------------------------------------------------- 1 | import { readJson } from '@utils/readJson' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | const failedPagesId = 'node_modules/.saus/failed-pages.json' 6 | 7 | export function getFailedPages(): string[] { 8 | try { 9 | return readJson(failedPagesId) 10 | } catch { 11 | return [] 12 | } 13 | } 14 | 15 | export function setFailedPages(pagePaths: string[]) { 16 | fs.mkdirSync(path.dirname(failedPagesId), { recursive: true }) 17 | fs.writeFileSync(failedPagesId, JSON.stringify(pagePaths)) 18 | } 19 | -------------------------------------------------------------------------------- /src/build/runBundle.ts: -------------------------------------------------------------------------------- 1 | import { removeSourceMapUrls } from '@utils/node/sourceMap' 2 | import path from 'path' 3 | import vm from 'vm' 4 | 5 | export function runBundle({ 6 | code, 7 | filename, 8 | }: { 9 | code: string 10 | filename: string 11 | }) { 12 | const initialize: (exports: any, require: Function) => void = 13 | vm.runInThisContext( 14 | `(0, function(exports,require) {` + 15 | removeSourceMapUrls(code) + 16 | `\n})\n//# sourceMappingURL=${path.basename(filename)}.map\n`, 17 | { filename } 18 | ) 19 | 20 | const exports: any = {} 21 | initialize(exports, require) 22 | return exports as typeof import('../bundle/runtime/bundle/api') 23 | } 24 | -------------------------------------------------------------------------------- /src/build/worker.ts: -------------------------------------------------------------------------------- 1 | import { workerData } from 'worker_threads' 2 | import { loadPageFactory } from './pageFactory' 3 | 4 | export interface BuildWorker { 5 | renderPage(pageUrl: string): Promise | void 6 | destroy?: () => Promise 7 | } 8 | 9 | export default loadPageFactory(workerData) 10 | -------------------------------------------------------------------------------- /src/bundle/clientPreloads.ts: -------------------------------------------------------------------------------- 1 | import { clientPreloadsMarker } from '@/routeClients' 2 | import { dataToEsm } from '@runtime/dataToEsm' 3 | import { RETURN } from '@runtime/tokens' 4 | 5 | export function injectClientPreloads( 6 | code: string, 7 | preloads: string[], 8 | helpersModuleId: string 9 | ) { 10 | const preloadImport = `import {preloadModules} from "${helpersModuleId}"` 11 | const preloadCall = `preloadModules(${dataToEsm(preloads, '')})` 12 | return code.replace( 13 | clientPreloadsMarker, 14 | `${preloadImport};${RETURN}${preloadCall}` 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/bundle/html.ts: -------------------------------------------------------------------------------- 1 | // An entry point for @saus/html to expose helpers used by SSR bundles. 2 | export * from './html/inject' 3 | export * from './html/serialize' 4 | export * from './html/types' 5 | -------------------------------------------------------------------------------- /src/bundle/html/types.ts: -------------------------------------------------------------------------------- 1 | export interface HtmlTagDescriptor { 2 | tag: string 3 | attrs?: Record 4 | children?: string | HtmlTagDescriptor[] 5 | /** 6 | * default: 'head-prepend' 7 | */ 8 | injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend' 9 | } 10 | -------------------------------------------------------------------------------- /src/bundle/html/utils.ts: -------------------------------------------------------------------------------- 1 | export function incrementIndent(indent: string = '') { 2 | return `${indent}${indent[0] === '\t' ? '\t' : ' '}` 3 | } 4 | -------------------------------------------------------------------------------- /src/bundle/renderBundleModule.ts: -------------------------------------------------------------------------------- 1 | import { bundleDir } from '@/paths' 2 | import endent from 'endent' 3 | import path from 'path' 4 | 5 | export function renderBundleModule(ssrEntryId: string) { 6 | const runtimeId = path.join(bundleDir, 'bundle/api.mjs') 7 | const runtimeConfigId = path.join(bundleDir, 'bundle/config.mjs') 8 | return endent` 9 | import "${ssrEntryId}" 10 | 11 | export * from "${runtimeId}" 12 | export { default } from "${runtimeId}" 13 | export { default as config } from "${runtimeConfigId}" 14 | ` 15 | } 16 | -------------------------------------------------------------------------------- /src/bundle/routes/appVersion.ts: -------------------------------------------------------------------------------- 1 | import endent from 'endent' 2 | import { BundleContext } from '../context' 3 | 4 | /** 5 | * When the `appVersion` bundle option is defined, this plugin will 6 | * add a route to the bundle that responds with it. 7 | */ 8 | export function injectAppVersionRoute( 9 | appVersion: string, 10 | context: BundleContext 11 | ) { 12 | const routePath = '/.saus/app/version' 13 | context.injectedImports.prepend.push( 14 | context.injectedModules.addServerModule({ 15 | id: '\0@saus/routes/appVersion.js', 16 | code: endent` 17 | import { route, deployedEnv } from 'saus' 18 | 19 | deployedEnv.appVersion = "${appVersion}" 20 | 21 | route("${routePath}").get(req => { 22 | req.respondWith(200, { text: "${appVersion}" }) 23 | }) 24 | `, 25 | }).id 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/bundle/routes/clientStore.ts: -------------------------------------------------------------------------------- 1 | import { bundleDir } from '@/paths' 2 | import { injectRoutes } from '@/virtualRoutes' 3 | import { resolve } from 'path' 4 | import { BundleContext } from '../context' 5 | 6 | export function injectClientStoreRoute(context: BundleContext) { 7 | injectRoutes(context, [ 8 | { 9 | path: '/*', 10 | plugin: resolve(bundleDir, '../routes/clientStorePlugin.mjs'), 11 | }, 12 | ]) 13 | } 14 | -------------------------------------------------------------------------------- /src/bundle/runtime/README.md: -------------------------------------------------------------------------------- 1 | # src/bundle/runtime 2 | 3 | The `bundle/` and `core/` subfolders are only used in SSR bundles. They override the `saus/bundle` and `saus/core` entry modules, respectively. 4 | 5 | The remaining modules are intended for any Node.js environment, not just SSR bundles. 6 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/api.ts: -------------------------------------------------------------------------------- 1 | // Overrides "saus/bundle" entry in SSR bundles 2 | export type { 3 | App, 4 | RenderedFile, 5 | RenderedPage, 6 | RenderPageOptions, 7 | RenderPageResult, 8 | ResolvedRoute, 9 | } from '@runtime/app/types' 10 | export * from '@runtime/bundleTypes' 11 | export { setResponseCache } from '@runtime/http/responseCache' 12 | export { ssrImport, __d as ssrDefine } from '@runtime/ssrModules' 13 | export { printFiles } from '@utils/node/printFiles' 14 | export { createApp as default } from './app' 15 | export { loadAsset, loadModule } from './clientStore' 16 | export { default as config } from './config' 17 | export { configureBundle } from './context' 18 | export { getKnownPaths } from './paths' 19 | export * from './server' 20 | export { writePages } from './writePages' 21 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/clientAssets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inlined assets are encoded with Base64. 3 | * 4 | * If assets are not inlined, the value is an ETag header. 5 | */ 6 | const clientAssets: Record = (globalThis as any) 7 | .sausClientAssets 8 | 9 | // Stub module replaced at build time. 10 | export default clientAssets 11 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/clientEntries.ts: -------------------------------------------------------------------------------- 1 | type ClientEntryId = string 2 | 3 | export type ClientEntries = { 4 | [layoutModuleId: string]: { 5 | [routeModuleId: string]: ClientEntryId 6 | } 7 | } 8 | 9 | /** 10 | * Every combination of layout module + route module has its own 11 | * entry module loaded by the client, except for layouts without 12 | * a hydrator module. This mapping is used by the SSR bundle to 13 | * find the public URL for each entry module. 14 | * 15 | * The keys are SSR paths, which are identical to dev server paths. 16 | */ 17 | const clientEntries: ClientEntries = (globalThis as any).sausClientEntries 18 | 19 | // Stub module replaced at build time. 20 | export default clientEntries 21 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/clientModules.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * If modules are not inlined, the value is an ETag header. 3 | */ 4 | const clientModules: Record = (globalThis as any) 5 | .sausClientModules 6 | 7 | // Stub module replaced at build time. 8 | export default clientModules 9 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/clientStore/index.ts: -------------------------------------------------------------------------------- 1 | export * from './inline' 2 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/clientStore/inline.ts: -------------------------------------------------------------------------------- 1 | import clientAssets from '../clientAssets' 2 | import clientModules from '../clientModules' 3 | 4 | export async function loadModule(id: string) { 5 | return clientModules[id] 6 | } 7 | 8 | export async function loadAsset(id: string): Promise { 9 | return Buffer.from(clientAssets[id], 'base64') 10 | } 11 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/clientStore/local.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { promisify } from 'util' 3 | 4 | const readFile = promisify(fs.readFile) 5 | 6 | // Assume the working directory is the `build.outDir` option. 7 | export function loadModule(id: string): Promise { 8 | return readFile(id, 'utf8') 9 | } 10 | 11 | // Assume the working directory is the `build.outDir` option. 12 | export function loadAsset(id: string): Promise { 13 | return readFile(id) 14 | } 15 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/clientStyles.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The CSS required by each route client. 3 | */ 4 | const clientStyles: Record = (globalThis as any) 5 | .sausClientStyles 6 | 7 | // Stub module replaced at build time. 8 | export default clientStyles 9 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/config.ts: -------------------------------------------------------------------------------- 1 | import type { RuntimeConfig } from '@runtime/config' 2 | 3 | /** 4 | * Configures the runtime behavior of the SSR bundle. 5 | * 6 | * Can be extended by Saus plugins. 7 | */ 8 | const config: RuntimeConfig = (globalThis as any).sausRuntimeConfig 9 | 10 | // Stub module replaced at build time 11 | export default config 12 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/context.ts: -------------------------------------------------------------------------------- 1 | import type { App } from '@runtime/app/types' 2 | import { createCache } from '@runtime/cache/create' 3 | import type { MutableRuntimeConfig } from '@runtime/config' 4 | import { ssrImport } from '@runtime/ssrModules' 5 | import config from './config' 6 | 7 | export const context: App.Context = { 8 | config, 9 | pageCache: createCache(), 10 | onError: console.error, 11 | routes: [], 12 | runtimeHooks: [], 13 | ssrRequire: ssrImport, 14 | } 15 | 16 | /** 17 | * Update the bundle's runtime config. 18 | */ 19 | export function configureBundle(update: Partial): void { 20 | if ('profile' in update) { 21 | context.profile = update.profile 22 | delete update.profile 23 | } 24 | Object.assign(config, update) 25 | } 26 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/debug.ts: -------------------------------------------------------------------------------- 1 | // In worker environments, the `debug` package does nothing. 2 | export default () => () => {} 3 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/debugBase.ts: -------------------------------------------------------------------------------- 1 | import { createHtmlResolver, createVisitor } from '@runtime/html' 2 | import config from './config' 3 | 4 | /** 5 | * Scan the `` tree for internal links to be rewritten 6 | * so they point to the debug-view equivalent URL. 7 | */ 8 | export function injectDebugBase( 9 | debugBase: string 10 | ): (html: string, state: any) => Promise { 11 | const resolver = createHtmlResolver( 12 | id => 13 | id.startsWith(config.base) && !id.startsWith(debugBase) 14 | ? id.replace(config.base, debugBase) 15 | : null, 16 | // Rewrite anchor elements only. 17 | { a: ['href'] } 18 | ) 19 | return createVisitor({ 20 | body(body) { 21 | body.traverse(resolver) 22 | }, 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/injectSausClient.ts: -------------------------------------------------------------------------------- 1 | import * as clientNodeAPI from '@client/node/api' 2 | import clientRoutes from '@client/routes' 3 | import { __d } from '@runtime/ssrModules' 4 | 5 | /** 6 | * Set the exports of `saus/client` used by isolated SSR modules. 7 | */ 8 | export function injectSausClient(overrides?: Record) { 9 | __d('saus/client', async __exports => { 10 | __exports.routes = clientRoutes 11 | Object.assign(__exports, clientNodeAPI) 12 | overrides && Object.assign(__exports, overrides) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/routes.ts: -------------------------------------------------------------------------------- 1 | // Stub module. Replaced during build. 2 | const routes: Record = {} 3 | export default routes 4 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/server/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const debug = createDebug('saus:server') 4 | -------------------------------------------------------------------------------- /src/bundle/runtime/bundle/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './connect' 2 | export * from './fileCache' 3 | export * from './serveCachedFiles' 4 | export * from './servePages' 5 | export * from './servePublicDir' 6 | -------------------------------------------------------------------------------- /src/bundle/runtime/client/api.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL, isDebug } from '@client/baseUrl' 2 | import { prependBase as prepend } from '@utils/base' 3 | 4 | export * from '@client/node/api' 5 | export { default as routes } from '@client/routes' 6 | export { BASE_URL, isDebug } 7 | 8 | export function prependBase(uri: string, base = BASE_URL) { 9 | return prepend(uri, base) 10 | } 11 | 12 | export const applyHead = unsupportedFn('applyHead') 13 | 14 | function unsupportedFn(name: string) { 15 | return () => { 16 | throw Error( 17 | `Cannot call "${name}" in SSR environment. ` + 18 | `Wrap the call with \`if (!${'import.meta'}.env.SSR)\` to avoid it.` 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/bundle/runtime/core/api.ts: -------------------------------------------------------------------------------- 1 | // This overrides the "saus/core" entry in SSR bundles. 2 | import { setRoutesModule } from '@runtime/global' 3 | import { context } from '../bundle/context' 4 | 5 | export * from '@/api' 6 | export * from '@runtime/ssrModules' 7 | export * from '@vm/esmInterop' 8 | 9 | // In SSR bundles, these globals are mutated at the top level 10 | // immediately, so they need to be defined now. 11 | setRoutesModule(context) 12 | 13 | // This is also exported by "saus/src/client" but we want to avoid 14 | // processing that module, since it has heavy dependencies that bog down 15 | // Rollup. 16 | export const defineClient = (x: any) => x 17 | -------------------------------------------------------------------------------- /src/bundle/runtime/core/constants.ts: -------------------------------------------------------------------------------- 1 | // This module overrides @/constants.ts 2 | 3 | export const compact = true 4 | -------------------------------------------------------------------------------- /src/bundle/runtime/defineSecrets.ts: -------------------------------------------------------------------------------- 1 | import { deployedEnv } from '@runtime/deployedEnv' 2 | import type { SecretMap } from '../../secrets/types' 3 | 4 | export function defineSecrets(secrets: SecretMap) { 5 | // Secret access is forwarded to the deployedEnv object, 6 | // which should be populated by external packages. 7 | return new Proxy(deployedEnv, { 8 | get(_, key: string) { 9 | key = secrets[key] 10 | if (key) { 11 | const value = deployedEnv[key] 12 | if (value === undefined) { 13 | throw Error('Missing secret: ' + key) 14 | } 15 | return value 16 | } 17 | }, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import cac from 'cac' 2 | import * as inspector from 'inspector' 3 | import * as actions from './cli/actions' 4 | import { useCommands } from './cli/command' 5 | 6 | declare const globalThis: any 7 | if (inspector.url()) { 8 | globalThis.__inspectorActive = true 9 | } 10 | 11 | declare const __VERSION__: string 12 | 13 | const cli = cac('saus') 14 | useCommands(cli, actions) 15 | 16 | cli.help() 17 | cli.version(__VERSION__) 18 | 19 | export default cli 20 | -------------------------------------------------------------------------------- /src/cli/actions/deploy/default.ts: -------------------------------------------------------------------------------- 1 | import { command } from '../../command' 2 | 3 | command(deploy) 4 | .option('-n, --dry-run', `[boolean] enable dry logs and skip deploying`) 5 | .option('--no-cache', `[boolean] avoid using cached build artifacts`) 6 | .option('--no-revert', `[boolean] skip rollbacks if deployment fails`) 7 | 8 | export type DeployFlags = { 9 | dryRun?: true 10 | cache?: false 11 | revert?: false 12 | } 13 | 14 | export { deploy as default } 15 | 16 | async function deploy(options: DeployFlags) { 17 | const { deploy } = await import('../../../deploy/api.js') 18 | await deploy({ 19 | ...options, 20 | noCache: options.cache === false, 21 | noRevert: options.revert === false, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/cli/actions/deploy/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './default' 2 | export * from './sync' 3 | 4 | -------------------------------------------------------------------------------- /src/cli/actions/deploy/sync.ts: -------------------------------------------------------------------------------- 1 | import { command } from '../../command' 2 | 3 | command(sync) 4 | 5 | export { sync } 6 | 7 | async function sync() { 8 | const { loadDeployContext } = await import('../../../deploy/context.js') 9 | const context = await loadDeployContext() 10 | await context.syncDeployCache() 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/actions/dev.ts: -------------------------------------------------------------------------------- 1 | import { vite } from '@/vite' 2 | import { command } from '../command' 3 | 4 | command(dev) 5 | .option('--host [host]', `[string] specify hostname`) 6 | .option('--port ', `[number] specify port`) 7 | .option('--open [path]', `[boolean | string] open browser on startup`) 8 | .option('--strictPort', `[boolean] exit if specified port is already in use`) 9 | .option( 10 | '--force', 11 | `[boolean] force the optimizer to ignore the cache and re-bundle` 12 | ) 13 | 14 | export async function dev(options: vite.ServerOptions) { 15 | const { createServer } = await import('../../dev/api.js') 16 | await createServer({ server: options }) 17 | } 18 | -------------------------------------------------------------------------------- /src/cli/actions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build' 2 | export * from './bundle' 3 | export * as deploy from './deploy' 4 | export * from './dev' 5 | export * from './preview' 6 | export * as secrets from './secrets' 7 | export * from './test' 8 | -------------------------------------------------------------------------------- /src/cli/actions/preview.ts: -------------------------------------------------------------------------------- 1 | import type { PreviewOptions } from '../../preview/options' 2 | import { command } from '../command' 3 | 4 | command(preview) 5 | .option('--host [host]', `[string] specify hostname`) 6 | .option('--port ', `[number] specify port`) 7 | .option('--strictPort', `[boolean] exit if specified port is already in use`) 8 | .option('--https', `[boolean] use TLS + HTTP/2`) 9 | .option('--open [path]', `[boolean | string] open browser on startup`) 10 | 11 | export async function preview(options: PreviewOptions) { 12 | const { startPreviewServer } = await import('../../preview/api.js') 13 | const server = await startPreviewServer(options) 14 | server.printUrls() 15 | } 16 | -------------------------------------------------------------------------------- /src/cli/actions/secrets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './add' 2 | export * from './ls' 3 | export * from './rm' 4 | export * from './set' 5 | -------------------------------------------------------------------------------- /src/cli/actions/secrets/ls.ts: -------------------------------------------------------------------------------- 1 | import { gray } from 'kleur/colors' 2 | import { command } from '../../command' 3 | 4 | command(listSecrets) // 5 | 6 | export { listSecrets as ls } 7 | 8 | async function listSecrets() { 9 | const { loadDeployContext, loadSecretSources } = await import( 10 | '../../../secrets/api.js' 11 | ) 12 | 13 | const context = await loadDeployContext({ 14 | command: 'secrets', 15 | }) 16 | 17 | await loadSecretSources(context) 18 | await context.secrets.load() 19 | 20 | const secrets = context.secrets['_secrets'] 21 | if (Object.keys(secrets).length) { 22 | for (const [name, value] of Object.entries(secrets)) { 23 | console.log(name + ' = %O', value) 24 | } 25 | } else { 26 | process.stderr.write('\n\n' + gray('No secrets found.') + '\n') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/client/README.md: -------------------------------------------------------------------------------- 1 | # @saus/client 2 | 3 | This folder contains modules meant for browser and web worker environments only. 4 | -------------------------------------------------------------------------------- /src/client/api.ts: -------------------------------------------------------------------------------- 1 | // These modules are suitable for both client-side and server-side use. 2 | 3 | export * from '@runtime/cache/create' 4 | export * from '@runtime/cache/global' 5 | export * from '@runtime/cache/types' 6 | export * from '@runtime/getPagePath' 7 | export * from '@runtime/mapStateModule' 8 | export * from '@runtime/parseRoutePath' 9 | export * from '@runtime/renderRoutePath' 10 | export * from '@runtime/stateModules' 11 | export * from '@utils/buffer' 12 | export * from '@utils/joinUrl' 13 | export * from '@utils/resolveModules' 14 | export * from '@utils/unwrapDefault' 15 | -------------------------------------------------------------------------------- /src/client/baseUrl.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This variable is like `import.meta.env.BASE_URL` except it 3 | * equals the `debugBase` option when in debug view. 4 | * 5 | * You **must not** use this for asset URLs or you will break 6 | * them in the debug view. Only use this variable for links. 7 | */ 8 | export const BASE_URL = import.meta.env.BASE_URL 9 | 10 | /** 11 | * Equals true when rendering in debug view. 12 | */ 13 | export const isDebug = false 14 | -------------------------------------------------------------------------------- /src/client/defineLayout.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLayout } from '@runtime/layouts' 2 | 3 | export function defineLayout(layout: RouteLayout) { 4 | return layout 5 | } 6 | -------------------------------------------------------------------------------- /src/client/dist/baseUrl.cjs: -------------------------------------------------------------------------------- 1 | // Stub module updated by the SSR dev runtime. 2 | exports.BASE_URL = '/' 3 | exports.isDebug = false 4 | -------------------------------------------------------------------------------- /src/client/dist/index.cjs: -------------------------------------------------------------------------------- 1 | // This module is only used by the SSR dev server. 2 | // In production SSR, this module is injected at runtime. 3 | Object.assign(exports, require('./node/api.js')) 4 | Object.assign(exports, require('./baseUrl.cjs')) 5 | exports.routes = require('./routes.cjs') 6 | -------------------------------------------------------------------------------- /src/client/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../index' 2 | -------------------------------------------------------------------------------- /src/client/dist/node/context.cjs: -------------------------------------------------------------------------------- 1 | // Stub module replaced in development. 2 | -------------------------------------------------------------------------------- /src/client/dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "exports": { 3 | ".": { 4 | "node": "./index.cjs", 5 | "types": "./index.d.ts", 6 | "import": "./index.mjs", 7 | "default": "./index.js" 8 | }, 9 | "./*": { 10 | "types": "./*.d.ts", 11 | "import": "./*.mjs", 12 | "default": "./*.js" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/client/dist/routes.cjs: -------------------------------------------------------------------------------- 1 | // Stub module updated by the SSR dev runtime. 2 | exports.loaders = {} 3 | -------------------------------------------------------------------------------- /src/client/dynamicImport.ts: -------------------------------------------------------------------------------- 1 | // We need this to avoid `import(...)` from being transformed into a 2 | // `require` call by Esbuild. 3 | export const dynamicImport = new Function('file', 'return import(file)') 4 | -------------------------------------------------------------------------------- /src/client/helpers.ts: -------------------------------------------------------------------------------- 1 | export { describeHead } from './head' 2 | export { hydrate } from './hydrate' 3 | export { importState } from './importState' 4 | export { preCacheState } from './preCacheState' 5 | export { preloadModules } from './preloadModules' 6 | -------------------------------------------------------------------------------- /src/client/importState.ts: -------------------------------------------------------------------------------- 1 | import { globalCache } from '@runtime/cache/global' 2 | import { preCacheState } from './preCacheState' 3 | import { preHydrateCache } from './stateModules/hydrate' 4 | 5 | /** 6 | * Wait for these state modules to be hydrated. 7 | */ 8 | export async function importState(...cacheKeys: string[]) { 9 | await preCacheState(...cacheKeys) 10 | return Promise.all( 11 | cacheKeys.map(async cacheKey => { 12 | if (preHydrateCache.has(cacheKey)) { 13 | await new Promise(resolve => { 14 | const name = cacheKey.replace(/(\.\d+)?$/, '') 15 | globalCache.listeners[name] ||= new Set() 16 | globalCache.listeners[name].add(resolve) 17 | }) 18 | } 19 | return globalCache.loaded[cacheKey] 20 | }) 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/client/index.dev.ts: -------------------------------------------------------------------------------- 1 | export * from './renderErrorPage' 2 | -------------------------------------------------------------------------------- /src/client/index.prod.ts: -------------------------------------------------------------------------------- 1 | export function renderErrorPage() {} 2 | -------------------------------------------------------------------------------- /src/client/index.ts: -------------------------------------------------------------------------------- 1 | import './context' 2 | 3 | export * from '@runtime/clientTypes' 4 | export * from './api' 5 | export * from './baseUrl' 6 | export * from './defineLayout' 7 | export * from './head' 8 | export * from './http/get' 9 | export * from './hydrate' 10 | export * from './index.dev' 11 | export * from './loadPageState' 12 | export * from './pageClient' 13 | export * from './prependBase' 14 | export { default as routes } from './routes' 15 | -------------------------------------------------------------------------------- /src/client/isDebug.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from './baseUrl' 2 | 3 | /** Equals true if the current URL is a debug page */ 4 | export const isDebug = BASE_URL !== import.meta.env.BASE_URL 5 | -------------------------------------------------------------------------------- /src/client/node/README.md: -------------------------------------------------------------------------------- 1 | # saus/src/client/node 2 | 3 | The modules in this folder are used in dev mode SSR only. 4 | -------------------------------------------------------------------------------- /src/client/node/api.ts: -------------------------------------------------------------------------------- 1 | // This module overrides "saus/client" on the server-side. 2 | export * from '../api' 3 | export * from './loadPageState' 4 | export * from './pageClient' 5 | -------------------------------------------------------------------------------- /src/client/node/loadPageState.ts: -------------------------------------------------------------------------------- 1 | import { globalCache } from '@runtime/cache/global' 2 | 3 | // This only exists to override the client implementation. 4 | export const loadPageState = globalCache.get.bind(globalCache) 5 | -------------------------------------------------------------------------------- /src/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/client", 3 | "version": "0.4.10", 4 | "scripts": { 5 | "clean": "rimraf dist && git checkout HEAD dist", 6 | "build": "npm run clean && tsup-node && tsc -p . --emitDeclarationOnly", 7 | "dev": "tsup-node --watch --sourcemap" 8 | }, 9 | "dependencies": { 10 | "@saus/runtime": "workspace:*", 11 | "@saus/utils": "workspace:*", 12 | "@saus/vm": "workspace:*" 13 | }, 14 | "devDependencies": { 15 | "@runtime": "link:./node_modules/@saus/runtime/dist", 16 | "@utils": "link:./node_modules/@saus/utils/dist", 17 | "@vm": "link:./node_modules/@saus/vm/dist" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/preCacheState.ts: -------------------------------------------------------------------------------- 1 | import saus from './context' 2 | import { dynamicImport } from './dynamicImport' 3 | import { prependBase } from './prependBase' 4 | 5 | /** 6 | * Preload a page's data into the global RAM cache. 7 | * 8 | * Note: This doesn't support on-demand state modules, so it should 9 | * only be used for state modules coupled to the page's route, which 10 | * means the state module is loaded unconditionally when the page 11 | * is requested. 12 | */ 13 | export function preCacheState(...cacheKeys: string[]) { 14 | return Promise.all( 15 | cacheKeys.map(cacheKey => { 16 | const stateUrl = prependBase(saus.stateModuleBase + cacheKey + '.js') 17 | return dynamicImport(stateUrl) 18 | }) 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/client/preloadModules.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from './baseUrl' 2 | import { injectLinkTag } from './head' 3 | 4 | export function preloadModules(urls: string[]) { 5 | for (const url of urls) 6 | injectLinkTag( 7 | BASE_URL + url, 8 | url.endsWith('.css') ? 'stylesheet' : undefined 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/client/prependBase.ts: -------------------------------------------------------------------------------- 1 | const trailingSlash = /\/$/ 2 | 3 | export function prependBase(uri: string, base = import.meta.env.BASE_URL) { 4 | return base.replace(trailingSlash, uri) 5 | } 6 | -------------------------------------------------------------------------------- /src/client/renderErrorPage.ts: -------------------------------------------------------------------------------- 1 | import { renderErrorFallback } from '@runtime/app/errorFallback' 2 | 3 | export function renderErrorPage(e: any) { 4 | const errorElem = document.createElement('div') 5 | const style = [ 6 | 'visibility: hidden', 7 | 'position: fixed', 8 | 'top: 0', 9 | 'left: 0', 10 | 'width: 100vw', 11 | 'height: 100vh', 12 | 'z-index: 9999', 13 | ] 14 | errorElem.setAttribute('style', style.join(';')) 15 | const shadowRoot = errorElem.attachShadow({ mode: 'open' }) 16 | shadowRoot.innerHTML = renderErrorFallback(e, { 17 | root: saus.devRoot, 18 | origin: location.origin, 19 | }) 20 | document.body.appendChild(errorElem) 21 | requestIdleCallback(() => { 22 | errorElem.style.visibility = '' 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/client/renderPage.ts: -------------------------------------------------------------------------------- 1 | import { getPagePath } from '@runtime/getPagePath' 2 | import { RouteParams } from '@runtime/routeTypes' 3 | import { loadPageClient, PageClient } from './pageClient' 4 | import { prependBase } from './prependBase' 5 | 6 | export async function renderPage( 7 | routePath: string, 8 | routeParams?: RouteParams, 9 | client?: PageClient 10 | ): Promise { 11 | client ||= await loadPageClient(routePath, routeParams) 12 | const pagePath = getPagePath(routePath, routeParams) 13 | const pageUrl = new URL(location.origin + prependBase(pagePath)) 14 | return client.layout.render({ 15 | module: client.routeModule, 16 | params: routeParams || {}, 17 | path: pageUrl.pathname, 18 | props: client.props, 19 | query: pageUrl.search, 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/client/routes.ts: -------------------------------------------------------------------------------- 1 | // Stub module. Replaced during build. 2 | const routes: Record = {} 3 | export default routes 4 | -------------------------------------------------------------------------------- /src/client/stateModules/get.ts: -------------------------------------------------------------------------------- 1 | import type { Cache } from '@runtime/cache/types' 2 | import { getLoadedStateOrThrow } from '@runtime/getLoadedStateOrThrow' 3 | import { getStateModuleKey } from '@runtime/getStateModuleKey' 4 | import type { StateModule } from '@runtime/stateModules' 5 | 6 | export function getState( 7 | cache: Cache, 8 | module: StateModule, 9 | args: Args 10 | ) { 11 | const key = getStateModuleKey(module, args) 12 | return getLoadedStateOrThrow(cache, key, args) 13 | } 14 | -------------------------------------------------------------------------------- /src/client/stateModules/global.ts: -------------------------------------------------------------------------------- 1 | import { stateModulesByName } from '@runtime/cache/global' 2 | import type { StateModule } from '@runtime/stateModules' 3 | import { hydrateState, preHydrateCache } from './hydrate' 4 | 5 | export function trackStateModule(module: StateModule) { 6 | // TODO: escape moduleIds for regex syntax 7 | const cacheKeyPattern = new RegExp('^(' + module.name + ')(\\.[^.]+)?$') 8 | for (const [key, served] of preHydrateCache) { 9 | if (cacheKeyPattern.test(key)) { 10 | hydrateState(key, served, module) 11 | preHydrateCache.delete(key) 12 | } 13 | } 14 | stateModulesByName.set(module.name, module) 15 | } 16 | -------------------------------------------------------------------------------- /src/client/textDecoder.ts: -------------------------------------------------------------------------------- 1 | export const TextDecoder = globalThis.TextDecoder 2 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "exclude": ["**/*.spec.ts", "dist", "tsup.config.ts"], 5 | "compilerOptions": { 6 | "incremental": true, 7 | "lib": ["dom", "esnext"], 8 | "module": "esnext", 9 | "outDir": "dist", 10 | "target": "esnext", 11 | "tsBuildInfoFile": "dist/.tsbuildinfo", 12 | "types": ["../dist/env/client"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/client/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions' 2 | import { defineConfig } from 'tsup' 3 | 4 | export default defineConfig({ 5 | entry: [ 6 | '**/*.ts', 7 | '!**/*.spec.ts', 8 | '!*.config.ts', 9 | '!node_modules/**', 10 | '!dist/**', 11 | ], 12 | outDir: 'dist', 13 | format: ['cjs', 'esm'], 14 | bundle: false, 15 | plugins: [esbuildPluginFilePathExtensions()], 16 | }) 17 | -------------------------------------------------------------------------------- /src/core/AssetStore.ts: -------------------------------------------------------------------------------- 1 | import { Http } from '@runtime/http' 2 | import { Promisable } from 'type-fest' 3 | 4 | /** 5 | * The `AssetStore` is a normalized object storage layer. 6 | */ 7 | export interface AssetStore { 8 | supportedHeaders?: string[] 9 | /** 10 | * Upsert an asset by its name. 11 | */ 12 | put( 13 | name: string, 14 | data: string | Buffer, 15 | headers?: Http.ResponseHeaders 16 | ): Promisable 17 | /** 18 | * Remove an asset by its name. 19 | */ 20 | delete(name: string): Promisable 21 | } 22 | -------------------------------------------------------------------------------- /src/core/api.ts: -------------------------------------------------------------------------------- 1 | // These exports are suitable to import from modules that 2 | // run in SSR bundles and/or during static builds. 3 | export { createFilter } from '@rollup/pluginutils' 4 | export * from '@runtime/clientTypes' 5 | export * from '@runtime/endpoint' 6 | export * from '@runtime/layoutRenderer' 7 | export * from '@runtime/requestMetadata' 8 | export * from '@runtime/routeTypes' 9 | export * from '@runtime/url' 10 | export { default as endent } from 'endent' 11 | export * from './AssetStore' 12 | export * from './cache' 13 | 14 | -------------------------------------------------------------------------------- /src/core/babel/exports.ts: -------------------------------------------------------------------------------- 1 | import { NodePath, t } from '@utils/babel' 2 | 3 | export function getExportDeclarations(program: NodePath) { 4 | return program 5 | .get('body') 6 | .filter(stmt => 7 | stmt.isExportDeclaration() 8 | ) as NodePath[] 9 | } 10 | -------------------------------------------------------------------------------- /src/core/babel/imports.ts: -------------------------------------------------------------------------------- 1 | import { NodePath, t } from '@utils/babel' 2 | 3 | export function getImportDeclaration( 4 | program: NodePath, 5 | moduleName: string 6 | ) { 7 | return program 8 | .get('body') 9 | .find( 10 | stmt => stmt.isImportDeclaration() && stmt.node.source.value == moduleName 11 | ) as NodePath | undefined 12 | } 13 | 14 | export function getImportDeclarations(program: NodePath) { 15 | return program 16 | .get('body') 17 | .filter(stmt => 18 | stmt.isImportDeclaration() 19 | ) as NodePath[] 20 | } 21 | -------------------------------------------------------------------------------- /src/core/babel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/babel", 3 | "private": true, 4 | "main": "./index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /src/core/cache.ts: -------------------------------------------------------------------------------- 1 | export { createCache } from '@runtime/cache/create' 2 | export * from '@runtime/cache/global' 3 | export type { Cache } from '@runtime/cache/types' 4 | export { injectCachePlugin } from '@runtime/cachePlugin' 5 | export type { CachePlugin } from '@runtime/cachePlugin' 6 | export { setState } from '@runtime/stateModules/setState' 7 | -------------------------------------------------------------------------------- /src/core/core.ts: -------------------------------------------------------------------------------- 1 | export * from './index' 2 | -------------------------------------------------------------------------------- /src/core/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const debug = /* @__PURE__ */ createDebug('saus') 4 | -------------------------------------------------------------------------------- /src/core/defineClient.ts: -------------------------------------------------------------------------------- 1 | import { ImportDescriptorMap } from '@runtime/imports' 2 | 3 | export function defineClient(description: ClientDescription) { 4 | return description 5 | } 6 | 7 | export interface ClientDescription { 8 | /** 9 | * Define `import` statements to be included. 10 | * 11 | * The keys are modules to import from, and the values are either the 12 | * identifier used for the default export or an array of identifiers 13 | * used for named exports. 14 | */ 15 | imports: ImportDescriptorMap 16 | /** 17 | * Hydration code to run on the client. 18 | * 19 | * Executed inside a function with this type signature: 20 | * 21 | * async (content: unknown, request: RenderRequest) => void 22 | * 23 | * Custom imports are available as well. 24 | */ 25 | onHydrate: string 26 | } 27 | -------------------------------------------------------------------------------- /src/core/getBundleHash.ts: -------------------------------------------------------------------------------- 1 | import { murmurHash } from '@utils/murmur3' 2 | import { pick, pickAllExcept } from '@utils/pick' 3 | import { BundleOptions } from '../bundle/options' 4 | import { BundleConfig } from './vite' 5 | 6 | export function getBundleHash( 7 | mode: string, 8 | config: BundleConfig, 9 | bundleOptions: BundleOptions 10 | ) { 11 | const values = { 12 | mode, 13 | ...pick(config, ['type', 'entry', 'target', 'format', 'clientStore']), 14 | bundle: pickAllExcept(bundleOptions, [ 15 | 'appVersion', 16 | 'forceWriteAssets', 17 | 'onPublicFile', 18 | 'publicDirMode', 19 | ]), 20 | } 21 | return murmurHash(JSON.stringify(values)) 22 | } 23 | -------------------------------------------------------------------------------- /src/core/getEntryModules.ts: -------------------------------------------------------------------------------- 1 | import { dedupe } from '@utils/dedupe' 2 | import { SausContext } from './context' 3 | 4 | export async function getEntryModules(context: SausContext) { 5 | const routes = [...context.routes] 6 | context.defaultRoute && routes.push(context.defaultRoute) 7 | context.catchRoute && routes.push(context.catchRoute) 8 | 9 | return ( 10 | await Promise.all( 11 | dedupe( 12 | routes 13 | .map(route => [ 14 | route.moduleId, 15 | route.layoutEntry || context.defaultLayout.id, 16 | ]) 17 | .flat() 18 | ).map(async moduleId => { 19 | if (moduleId) { 20 | const resolved = await context.resolveId(moduleId) 21 | return resolved?.id 22 | } 23 | }) 24 | ) 25 | ).filter(Boolean) as string[] 26 | } 27 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * as esbuild from 'esbuild' 2 | export * from './api' 3 | export * from './defineClient' 4 | export * from './loadBundle' 5 | export * from './publicDir' 6 | export * from './setEnvData' 7 | export * from './virtualRoutes' 8 | export * from './vite' 9 | export * from './vite/esbuildPlugin' 10 | export * from './vite/functions' 11 | export * from './writeBundle' 12 | -------------------------------------------------------------------------------- /src/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/core", 3 | "private": true, 4 | "main": "./index.ts" 5 | } 6 | -------------------------------------------------------------------------------- /src/core/profiling.ts: -------------------------------------------------------------------------------- 1 | import elaps from 'elaps' 2 | import { debug } from './debug' 3 | 4 | export const Profiling: { mark(name: string): void } = process.env.PROFILE 5 | ? elaps() 6 | : { mark: debug } 7 | -------------------------------------------------------------------------------- /src/core/routeEntries.ts: -------------------------------------------------------------------------------- 1 | import { dataToEsm } from '@runtime/dataToEsm' 2 | import type { RouteRenderer } from '@runtime/routeTypes' 3 | import endent from 'endent' 4 | 5 | export function renderRouteEntry({ 6 | routes, 7 | routeModuleId, 8 | layoutModuleId, 9 | }: RouteRenderer) { 10 | return endent` 11 | export { default as layout } from "${layoutModuleId}" 12 | export * as routeModule from "${routeModuleId}" 13 | export ${dataToEsm( 14 | Array.from(routes, route => route.path), 15 | 'routes' 16 | )} 17 | ` 18 | } 19 | -------------------------------------------------------------------------------- /src/core/setEnvData.ts: -------------------------------------------------------------------------------- 1 | import { deployedEnv, DeployedEnv } from '@runtime/deployedEnv' 2 | import { PartialDeep } from 'type-fest' 3 | import { getDeployContext } from '../deploy' 4 | 5 | /** 6 | * Provide JSON values to the production SSR bundle 7 | * through the `deployedEnv` object. 8 | * 9 | * Call this during `saus deploy` only. 10 | */ 11 | export function setEnvData(env: PartialDeep) { 12 | if (getDeployContext()) { 13 | Object.assign(deployedEnv, env) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/core/testPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from './vite' 2 | 3 | export interface TestPlugin { 4 | /** Dev server plugins */ 5 | plugins?: Plugin[] 6 | /** A file was changed. */ 7 | onFileChange?: () => void 8 | /** The dev server was restarted. */ 9 | onRestart?: () => void 10 | } 11 | 12 | export function defineTestPlugin(testPlugin: TestPlugin) { 13 | return testPlugin 14 | } 15 | -------------------------------------------------------------------------------- /src/core/vite/checkPublicFile.ts: -------------------------------------------------------------------------------- 1 | import { vite } from '@/vite' 2 | import { cleanUrl } from '@utils/cleanUrl' 3 | import fs from 'fs' 4 | import path from 'path' 5 | 6 | export function checkPublicFile( 7 | url: string, 8 | { publicDir }: vite.ResolvedConfig 9 | ): string | undefined { 10 | // note if the file is in /public, the resolver would have returned it 11 | // as-is so it's not going to be a fully resolved path. 12 | if (!publicDir || !url.startsWith('/')) { 13 | return 14 | } 15 | const publicFile = path.join(publicDir, cleanUrl(url)) 16 | if (fs.existsSync(publicFile)) { 17 | return publicFile 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/core/vite/collectCss.ts: -------------------------------------------------------------------------------- 1 | import { isCSSRequest } from '@utils/isCSSRequest' 2 | import { vite } from '../vite' 3 | 4 | export async function collectCss( 5 | mod: vite.ModuleNode, 6 | server: vite.ViteDevServer, 7 | urls = new Set(), 8 | seen = new Set() 9 | ) { 10 | if (mod.url && !seen.has(mod.url)) { 11 | seen.add(mod.url) 12 | if (isCssModule(mod)) { 13 | urls.add(mod) 14 | } 15 | if (!mod.transformResult) { 16 | await server.transformRequest(mod.url.replace(/^\/@id\//, '')) 17 | } 18 | await Promise.all( 19 | Array.from(mod.importedModules, dep => { 20 | return collectCss(dep, server, urls, seen) 21 | }) 22 | ) 23 | } 24 | return urls 25 | } 26 | 27 | function isCssModule(mod: vite.ModuleNode) { 28 | return isCSSRequest(mod.url) || (mod.id && /\?vue&type=style/.test(mod.id)) 29 | } 30 | -------------------------------------------------------------------------------- /src/core/vite/configFile.ts: -------------------------------------------------------------------------------- 1 | import { SausCommand } from '../context' 2 | import { vite } from '../vite' 3 | import { getConfigEnv } from './config' 4 | 5 | export const loadConfigFile = ( 6 | command: SausCommand, 7 | configFile?: string, 8 | inlineConfig: vite.InlineConfig = {} 9 | ) => 10 | vite.loadConfigFromFile( 11 | getConfigEnv(command, inlineConfig.mode), 12 | configFile, 13 | inlineConfig.root, 14 | inlineConfig.logLevel 15 | ) 16 | -------------------------------------------------------------------------------- /src/core/vite/modulePreload.ts: -------------------------------------------------------------------------------- 1 | import type { vite } from '../vite' 2 | 3 | export function getPreloadTagsForModules( 4 | moduleUrls: Iterable, 5 | headTags: vite.HtmlTagDescriptor[] 6 | ) { 7 | for (const moduleUrl of moduleUrls) { 8 | headTags.push({ 9 | injectTo: 'head', 10 | tag: 'link', 11 | attrs: { 12 | rel: 'modulepreload', 13 | href: moduleUrl, 14 | }, 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/vite/requireHook.ts: -------------------------------------------------------------------------------- 1 | import Module from 'module' 2 | 3 | const NodeModule: { 4 | _resolveFilename(id: string, parent: NodeModule, ...rest: any[]): void 5 | } = Module as any 6 | 7 | const viteEntry = require.resolve('vite') 8 | 9 | const nodeResolve = NodeModule._resolveFilename 10 | NodeModule._resolveFilename = (id, parent, ...rest) => { 11 | // Force plugins to use our version of Vite. 12 | if (id === 'vite' && !parent.loaded) { 13 | return viteEntry 14 | } 15 | return nodeResolve(id, parent, ...rest) 16 | } 17 | -------------------------------------------------------------------------------- /src/core/vite/resolveEntryUrl.ts: -------------------------------------------------------------------------------- 1 | import type { vite } from '../vite' 2 | 3 | const FS_PREFIX = /^\/@fs\/\/?/ 4 | 5 | export function resolveEntryUrl(id: string, config: vite.ResolvedConfig) { 6 | return FS_PREFIX.test(id) 7 | ? id.replace(FS_PREFIX, '/') 8 | : id[0] === '/' 9 | ? config.root + id 10 | : id 11 | } 12 | -------------------------------------------------------------------------------- /src/core/vite/upsertPlugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from '../vite' 2 | 3 | const order = ['pre', undefined, 'post'] 4 | 5 | export function upsertPlugin(plugins: Plugin[], plugin: Plugin) { 6 | let newIndex = -1 7 | const priority = order.indexOf(plugin.enforce) 8 | const found = plugins.find((p, i, plugins: any) => { 9 | if (p.name === plugin.name) { 10 | plugins[i] = plugin 11 | return true 12 | } 13 | if (newIndex < 0 && priority <= order.indexOf(p.enforce)) { 14 | newIndex = i 15 | } 16 | }) 17 | if (!found) { 18 | plugins.splice(Math.max(newIndex, 0), 0, plugin) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/deploy/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const debug = createDebug('saus:deploy') 4 | -------------------------------------------------------------------------------- /src/deploy/helpers.ts: -------------------------------------------------------------------------------- 1 | import exec from '@cush/exec' 2 | 3 | /** 4 | * Get the current branch of `$PWD/.git` 5 | */ 6 | export function getCurrentGitBranch() { 7 | return exec.sync('git rev-parse --abbrev-ref HEAD') 8 | } 9 | -------------------------------------------------------------------------------- /src/deploy/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../secrets/runtime' 2 | export * from './bump' 3 | export * from './context' 4 | export * from './files' 5 | export * from './helpers' 6 | export * from './hooks' 7 | export * from './polling' 8 | export * from './prepareBundle' 9 | export { saveTargetCache } from './targetCache' 10 | export * from './types' 11 | -------------------------------------------------------------------------------- /src/deploy/options.ts: -------------------------------------------------------------------------------- 1 | import type { InlineBundleConfig } from '../bundle/context' 2 | 3 | export type DeployCommand = 'deploy' | 'secrets' 4 | 5 | export interface DeployOptions extends InlineBundleConfig { 6 | command?: DeployCommand 7 | /** 8 | * Deploy to a git respository other than `origin`. 9 | */ 10 | gitRepo?: { name: string; url: string } 11 | /** 12 | * Kill all deployed targets. 13 | */ 14 | killAll?: boolean 15 | /** 16 | * Skip the execution of any deployment action. 17 | */ 18 | dryRun?: boolean 19 | /** 20 | * Avoid using cached build artifacts. 21 | * 22 | * For example, the `loadBundle` function respects this. 23 | */ 24 | noCache?: boolean 25 | /** 26 | * Skip rollback functions on failure. 27 | */ 28 | noRevert?: boolean 29 | } 30 | -------------------------------------------------------------------------------- /src/deploy/prepareBundle.ts: -------------------------------------------------------------------------------- 1 | import { loadBundle, LoadBundleConfig } from '@/loadBundle' 2 | import { bumpAppVersion } from './bump' 3 | import { onDeploy } from './hooks' 4 | 5 | /** 6 | * Prepare the Saus SSR bundle for deployment. 7 | * 8 | * This function calls `onDeploy` so you don't have to. It also calls 9 | * the `bumpAppVersion` function, but only if the bundle isn't cached. 10 | */ 11 | export function prepareBundle( 12 | config?: LoadBundleConfig 13 | ): ReturnType { 14 | return onDeploy(() => 15 | loadBundle({ 16 | ...config, 17 | bundle: { 18 | publicDirMode: 'cache', 19 | ...config?.bundle, 20 | }, 21 | async onBundleStart(options) { 22 | const bump = await bumpAppVersion() 23 | if (bump.type) { 24 | options.appVersion = bump.version 25 | } 26 | }, 27 | }) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/deploy/revert.ts: -------------------------------------------------------------------------------- 1 | import { getDeployContext } from './context' 2 | import { RevertFn } from './types' 3 | 4 | export function onRevert(revertFn: RevertFn) { 5 | const ctx = getDeployContext() 6 | ctx?.revertFns.push(revertFn) 7 | } 8 | -------------------------------------------------------------------------------- /src/deploy/utils.ts: -------------------------------------------------------------------------------- 1 | import { omitKeys } from '@utils/keys' 2 | import { toObjectHash } from '@utils/objectHash' 3 | import { DeployPlugin, DeployTarget, DeployTargetId } from './types' 4 | 5 | export function omitEphemeral( 6 | state: Record, 7 | plugin: DeployPlugin 8 | ) { 9 | if (plugin.ephemeral) { 10 | return omitKeys(state, (_, key) => plugin.ephemeral!.includes(key)) 11 | } 12 | return state 13 | } 14 | 15 | export function defineTargetId( 16 | target: DeployTarget, 17 | values: Record 18 | ): asserts target is { _id: DeployTargetId } { 19 | if (target._id) return 20 | Object.defineProperty(target, '_id', { 21 | value: { hash: toObjectHash(values), values }, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/dev/events.ts: -------------------------------------------------------------------------------- 1 | import type { SausEvents } from '@/context' 2 | import { EventEmitter } from 'ee-ts' 3 | 4 | export interface DevEvents { 5 | listening(): void 6 | restart(message?: string): void 7 | close(): void 8 | error(e: any): void 9 | } 10 | 11 | export type DevEventEmitter = EventEmitter 12 | -------------------------------------------------------------------------------- /src/dist/bin/browserslist: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | baseDir=`dirname $0` 3 | binDir=`echo $PWD/node_modules/saus/node_modules/.pnpm/browserslist*` 4 | 5 | eval "$binDir/node_modules/browserslist/cli.js" --update-db 6 | -------------------------------------------------------------------------------- /src/dist/bin/saus: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | if (process.env.DEBUG) { 4 | require('source-map-support').install() 5 | if (/(^| )saus:\*( |$)/.test(process.env.DEBUG)) { 6 | process.env.DEBUG += ' saus' 7 | } 8 | } 9 | 10 | require('../cli').default.parse() 11 | -------------------------------------------------------------------------------- /src/dist/bundle/index.js: -------------------------------------------------------------------------------- 1 | // Stub module replaced by Saus at build time. 2 | -------------------------------------------------------------------------------- /src/dist/core/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../../core/index' 2 | -------------------------------------------------------------------------------- /src/dist/deploy/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../../deploy/index' 2 | -------------------------------------------------------------------------------- /src/dist/env/client.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare const saus: import('../client/context').ClientContext 4 | -------------------------------------------------------------------------------- /src/dist/env/node.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../index' 2 | -------------------------------------------------------------------------------- /src/dist/vite/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from 'vite' 2 | -------------------------------------------------------------------------------- /src/dist/vite/index.js: -------------------------------------------------------------------------------- 1 | Object.assign(exports, require('vite')) 2 | -------------------------------------------------------------------------------- /src/dist/vite/index.mjs: -------------------------------------------------------------------------------- 1 | export * from 'vite' 2 | -------------------------------------------------------------------------------- /src/preview/options.ts: -------------------------------------------------------------------------------- 1 | export interface PreviewOptions { 2 | host?: string | boolean 3 | https?: boolean 4 | open?: boolean | string 5 | port?: number 6 | strictPort?: boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/purge/index.ts: -------------------------------------------------------------------------------- 1 | export * from './purgeServerCache' 2 | export * from './route' 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /src/purge/purgeServerCache.ts: -------------------------------------------------------------------------------- 1 | import { globalCache } from '@runtime/cache/global' 2 | import { PurgePlugin } from './types' 3 | 4 | export function purgeServerCache(): PurgePlugin { 5 | return { 6 | name: 'saus:server-cache', 7 | purge(request) { 8 | if (request.globs.has('/*')) { 9 | return globalCache.clear() 10 | } 11 | // TODO: selective purge 12 | }, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/purge/request.ts: -------------------------------------------------------------------------------- 1 | import { getPageFilename } from '@utils/getPageFilename' 2 | import { PurgeOptions, PurgeRequest } from './types' 3 | 4 | export function makePurgeRequest( 5 | trigger: PurgeRequest['trigger'], 6 | options: PurgeOptions 7 | ) { 8 | const request: PurgeRequest = { 9 | trigger, 10 | files: new Set(options.files), 11 | paths: new Set(), 12 | globs: new Set(), 13 | } 14 | if (options.pages) { 15 | for (const path of options.pages) { 16 | if (path.includes('*')) { 17 | request.globs.add(path) 18 | } else { 19 | request.paths.add(path) 20 | if (!/\.[^.]+$/.test(path)) { 21 | const filename = getPageFilename(path) 22 | request.files.add(filename) 23 | request.files.add(filename + '.js') 24 | } 25 | } 26 | } 27 | } 28 | return request 29 | } 30 | -------------------------------------------------------------------------------- /src/runtime/README.md: -------------------------------------------------------------------------------- 1 | # @saus/runtime 2 | 3 | This folder contains isomorphic modules meant to run in both client and SSR environments, but not CLI/API environments (if it can be avoided). 4 | 5 | Global state usually lives in here. 6 | -------------------------------------------------------------------------------- /src/runtime/app/cacheClientProps.ts: -------------------------------------------------------------------------------- 1 | import { globalCache } from '../cache/global' 2 | import { App } from './types' 3 | 4 | export function cacheClientProps(maxAge: number): App.Plugin { 5 | return app => { 6 | const { loadPageProps } = app 7 | 8 | return { 9 | loadPageProps: (url, route) => 10 | globalCache.load(url.path, async cacheControl => { 11 | const props = await loadPageProps(url, route) 12 | cacheControl.maxAge = maxAge 13 | return props 14 | }), 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/runtime/app/cachePages.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '../cache/types' 2 | import { App, RenderPageResult } from './types' 3 | 4 | export function cachePages( 5 | maxAge: number, 6 | pageCache: Cache 7 | ): App.Plugin { 8 | return app => { 9 | const { renderPage } = app 10 | 11 | return { 12 | renderPage: (url, route, options) => 13 | pageCache.load(url.path, async cacheControl => { 14 | const page = await renderPage(url, route, options) 15 | cacheControl.maxAge = maxAge 16 | return page 17 | }), 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/runtime/app/collectStateFiles.ts: -------------------------------------------------------------------------------- 1 | import { toExpirationTime } from '../cache/expiration' 2 | import { App, LoadedStateModule, RenderedFile } from './types' 3 | 4 | export function collectStateFiles( 5 | files: RenderedFile[], 6 | loadedModules: LoadedStateModule[], 7 | app: App 8 | ): void { 9 | const { stateModuleBase } = app.config 10 | for (const loaded of loadedModules) { 11 | const { key, name } = loaded.stateModule 12 | files.push({ 13 | id: stateModuleBase + key + '.js', 14 | get data() { 15 | return app.renderStateModule(name, loaded) 16 | }, 17 | mime: 'application/javascript', 18 | expiresAt: toExpirationTime(loaded, undefined), 19 | wasCached: loaded.wasCached, 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/runtime/app/constants.ts: -------------------------------------------------------------------------------- 1 | import type { Http } from '../http' 2 | 3 | export const emptyArray: ReadonlyArray = Object.freeze([]) 4 | export const emptyHeaders: Readonly = Object.freeze({}) 5 | -------------------------------------------------------------------------------- /src/runtime/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cacheClientProps' 2 | export * from './cachePages' 3 | export * from './collectStateFiles' 4 | export * from './constants' 5 | export * from './createApp' 6 | export * from './errorFallback' 7 | export * from './logRequests' 8 | export * from './negotiator' 9 | export * from './throttleRender' 10 | export * from './types' 11 | -------------------------------------------------------------------------------- /src/runtime/bundleTypes.ts: -------------------------------------------------------------------------------- 1 | import type { RenderedFile, RenderedPage, RenderPageOptions } from './app/types' 2 | import type { ParsedUrl } from './url' 3 | 4 | export interface PageBundleOptions 5 | extends Pick { 6 | renderStart?: (url: ParsedUrl) => void 7 | renderFinish?: ( 8 | url: ParsedUrl, 9 | error: Error | null, 10 | page?: PageBundle | null 11 | ) => void 12 | /** @internal */ 13 | receivePage?: (page: RenderedPage | null, error: any) => void 14 | } 15 | 16 | export interface PageBundle { 17 | id: string 18 | html: string 19 | /** Files generated whilst rendering. */ 20 | files: RenderedFile[] 21 | } 22 | -------------------------------------------------------------------------------- /src/runtime/cache/clear.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from './types' 2 | 3 | /** 4 | * Traverse the given cache, removing any keys that match. 5 | */ 6 | export function clear( 7 | this: Cache, 8 | filter: string | ((key: string) => boolean) = () => true 9 | ) { 10 | const stores = [this.loaded, this.loading] 11 | if (typeof filter == 'function') { 12 | for (const store of stores) { 13 | for (const key of Object.keys(store)) { 14 | if (filter(key)) { 15 | delete store[key] 16 | } 17 | } 18 | } 19 | } else { 20 | for (const store of stores) { 21 | delete store[filter] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/runtime/cache/create.ts: -------------------------------------------------------------------------------- 1 | import { access, get, has, load } from './access' 2 | import { clear } from './clear' 3 | import { forEach } from './forEach' 4 | import { Cache } from './types' 5 | 6 | export const createCache = (): Cache => ({ 7 | listeners: {}, 8 | loading: {}, 9 | loaded: {}, 10 | has, 11 | get, 12 | load, 13 | access, 14 | clear, 15 | forEach, 16 | }) 17 | -------------------------------------------------------------------------------- /src/runtime/cache/expiration.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from './types' 2 | 3 | type Expirable = { 4 | timestamp: number 5 | maxAge?: Cache.MaxAge 6 | } 7 | 8 | export function toExpirationTime(entry: Expirable): number 9 | export function toExpirationTime( 10 | entry: Expirable, 11 | defaultAge: DefaultAge 12 | ): number | DefaultAge 13 | export function toExpirationTime(entry: Expirable, defaultAge?: number) { 14 | if (entry.maxAge == null) { 15 | return arguments.length == 1 ? Infinity : defaultAge 16 | } 17 | return entry.timestamp + entry.maxAge * 1000 18 | } 19 | 20 | export function toExpiresHeader(ts: number, maxAge?: Cache.MaxAge) { 21 | return maxAge != null && isFinite(maxAge) 22 | ? new Date(ts + maxAge).toUTCString() 23 | : undefined 24 | } 25 | -------------------------------------------------------------------------------- /src/runtime/cache/forEach.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from './types' 2 | 3 | export async function forEach( 4 | this: Cache, 5 | onEntry: (key: string, entry: Cache.Entry) => void, 6 | timeOutSecs?: number 7 | ): Promise { 8 | const loaded = Object.entries(this.loaded) 9 | const loading = Object.entries(this.loading) 10 | for (const [key, entry] of loaded) { 11 | onEntry(key, entry) 12 | } 13 | await Promise.all( 14 | loading.map(async ([key, promise]) => { 15 | onEntry(key, await promise) 16 | }) 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/runtime/cache/global.ts: -------------------------------------------------------------------------------- 1 | import type { StateModule } from '../stateModules' 2 | import { createCache } from './create' 3 | 4 | /** 5 | * All state in the global cache is meant to be used when rendering. 6 | * This means the state has been processed by `onLoad` listeners. 7 | */ 8 | export const globalCache = createCache() 9 | 10 | /** 11 | * State modules are stored here for data hydration and hot reloading 12 | * support. 13 | */ 14 | export const stateModulesByName = new Map() 15 | -------------------------------------------------------------------------------- /src/runtime/clientHooks.ts: -------------------------------------------------------------------------------- 1 | import { Promisable } from 'type-fest' 2 | import { RenderRequest } from './renderer' 3 | 4 | /** 5 | * UI framework plugins can extend this interface with declaration 6 | * merging to provide type safety to callers of the `defineLayout` 7 | * function. 8 | */ 9 | export interface ClientElement {} 10 | 11 | export interface ClientHooks { 12 | /** This layout is about to be hydrated. */ 13 | beforeHydrate?: (req: RenderRequest, root: ClientElement) => Promisable 14 | /** This layout has finished hydrating. */ 15 | afterHydrate?: (req: RenderRequest, root: ClientElement) => Promisable 16 | } 17 | -------------------------------------------------------------------------------- /src/runtime/clientTypes.ts: -------------------------------------------------------------------------------- 1 | import { AnyToObject } from '@utils/types' 2 | import type { RouteParams } from './routeTypes' 3 | 4 | export type AnyClientProps = CommonClientProps & Record 5 | 6 | /** JSON state provided by the renderer and made available to the client */ 7 | export interface CommonClientProps { 8 | rootId?: string 9 | routePath: string 10 | routeParams: AnyToObject 11 | error?: any 12 | } 13 | -------------------------------------------------------------------------------- /src/runtime/constants.ts: -------------------------------------------------------------------------------- 1 | // These constants are overridden for SSR mode. 2 | 3 | /** If true, use less whitespace when generating code. */ 4 | export const compact = false -------------------------------------------------------------------------------- /src/runtime/dataToEsm.spec.ts: -------------------------------------------------------------------------------- 1 | import { transformSync } from 'esbuild' 2 | import { describe, expect, it } from 'vitest' 3 | import { dataToEsm } from './dataToEsm' 4 | 5 | describe('dataToEsm', () => { 6 | it('handles multi-line strings', () => { 7 | const str = 'a\nb\nc\n' 8 | expect(evaluate(dataToEsm(str))).toBe(str) 9 | expect(evaluate(dataToEsm({ str })).str).toBe(str) 10 | expect(evaluate(dataToEsm([str]))[0]).toBe(str) 11 | expect(evaluate(dataToEsm({ title: '', test: [str] })).test[0]).toBe(str) 12 | }) 13 | }) 14 | 15 | function evaluate(code: string) { 16 | console.log({ code }) 17 | const transformed = transformSync(code, { loader: 'js', format: 'cjs' }) 18 | const init = new Function('module', 'exports', transformed.code) 19 | const mod: any = { exports: {} } 20 | init(mod, mod.exports) 21 | return mod.exports.default 22 | } 23 | -------------------------------------------------------------------------------- /src/runtime/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const debug = createDebug('saus:runtime') 4 | -------------------------------------------------------------------------------- /src/runtime/dist/app/errorFallbackClient.js: -------------------------------------------------------------------------------- 1 | import { createHotContext } from '/@id/@vite/client' 2 | 3 | createHotContext().on('vite:beforeFullReload', () => { 4 | document.body.innerHTML = '' 5 | }) 6 | 7 | document.addEventListener('click', event => { 8 | const { target } = event 9 | if (target.tagName == 'A' && target.className == 'file-link') { 10 | event.preventDefault() 11 | fetch(target.href) 12 | } 13 | }) 14 | 15 | -------------------------------------------------------------------------------- /src/runtime/dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "exports": { 3 | "./*": { 4 | "types": [ 5 | "./*.d.ts", 6 | "./*/index.d.ts" 7 | ], 8 | "import": [ 9 | "./*.mjs", 10 | "./*/index.mjs" 11 | ], 12 | "default": [ 13 | "./*.js", 14 | "./*/index.js" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/runtime/emptyModule.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/runtime/getLayoutEntry.ts: -------------------------------------------------------------------------------- 1 | import { parseLazyImport } from '@utils/parseLazyImport' 2 | import path from 'path' 3 | import type { Route } from './routeTypes' 4 | 5 | export function getLayoutEntry( 6 | route: Pick, 7 | defaultLayoutId: string 8 | ): string { 9 | if (typeof route.layout == 'function') { 10 | const layoutEntry = parseLazyImport(route.layout) 11 | if (!layoutEntry) { 12 | throw Error(`Failed to parse "layoutEntry" for route: "${route.path}"`) 13 | } 14 | if (route.file) { 15 | return path.resolve(path.dirname(route.file), layoutEntry) 16 | } 17 | return layoutEntry 18 | } 19 | return route.layout || defaultLayoutId 20 | } 21 | -------------------------------------------------------------------------------- /src/runtime/getLoadedStateOrThrow.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from './cache/types' 2 | 3 | export function getLoadedStateOrThrow( 4 | cache: Cache, 5 | cacheKey: string, 6 | args: readonly any[] 7 | ): Cache.Entry { 8 | const cached = cache.loaded[cacheKey] 9 | if (!cached) { 10 | const error = Error( 11 | `Failed to access "${cacheKey}" state module. ` + 12 | `Are you sure this route is configured to include it?` 13 | ) 14 | throw Object.assign(error, { 15 | code: 'STATE_MODULE_404', 16 | cacheKey, 17 | args, 18 | }) 19 | } 20 | return cached 21 | } 22 | -------------------------------------------------------------------------------- /src/runtime/getPagePath.ts: -------------------------------------------------------------------------------- 1 | import { renderRoutePath } from './renderRoutePath' 2 | import type { RouteParams } from './routeTypes' 3 | 4 | export function getPagePath( 5 | routePath: string, 6 | routeParams?: RouteParams | null 7 | ) { 8 | if (routeParams) { 9 | return renderRoutePath(routePath, routeParams) 10 | } 11 | return routePath 12 | } 13 | -------------------------------------------------------------------------------- /src/runtime/html/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const debug = createDebug('saus:html') 4 | -------------------------------------------------------------------------------- /src/runtime/html/index.ts: -------------------------------------------------------------------------------- 1 | export { minifyHtml } from './minify' 2 | export { parseHtml } from './parser' 3 | export { createHtmlResolver, resolveHtmlImports } from './resolver' 4 | export { $ } from './selector' 5 | export type { HtmlMatcher } from './selector' 6 | export { findTraverseVisitor, traverseHtml } from './traversal' 7 | export * from './types' 8 | export { bindVisitors as createVisitor } from './visitors/bind' 9 | export { sanitizeTag } from './xss' 10 | -------------------------------------------------------------------------------- /src/runtime/html/minify.ts: -------------------------------------------------------------------------------- 1 | import { setup } from '../setup' 2 | import { processHtml } from './process' 3 | 4 | /** 5 | * An ultra lightweight means of minifying the HTML of each page. 6 | * By default, this hook does nothing when `saus dev` is used. 7 | */ 8 | export const minifyHtml = ( 9 | options: { 10 | /** Minify in development too */ 11 | force?: boolean 12 | } = {} 13 | ) => 14 | setup( 15 | env => 16 | (options.force || env.command !== 'dev') && 17 | processHtml('post', { 18 | name: 'minifyHtml', 19 | process: html => 20 | html 21 | .replace(/(^|>)\s+([^\s])/g, '$1$2') 22 | .replace(/([^\s])\s+(<|$)/g, '$1$2'), 23 | }) 24 | ) 25 | -------------------------------------------------------------------------------- /src/runtime/html/symbols.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Avoid creating more than one `HtmlTagPath` instance 3 | * per AST node by storing the instance on the node. 4 | */ 5 | export const kTagPath = Symbol.for('html.TagPath') 6 | 7 | /** 8 | * Calls to the `traverseHtml` hook are combined to reduce 9 | * the number of full AST traversals to a maximum of 3. 10 | */ 11 | export const kVisitorsArray = Symbol.for('html.VisitorsArray') 12 | 13 | /** Indicates a removed or replaced node */ 14 | export const kRemovedNode = Symbol.for('html.RemovedNode') 15 | -------------------------------------------------------------------------------- /src/runtime/html/test.ts: -------------------------------------------------------------------------------- 1 | import { HtmlVisitor } from './types' 2 | import { bindVisitors } from './visitors/bind' 3 | 4 | /** Used for testing purposes */ 5 | export function traverse(html: string, visitors: HtmlVisitor | HtmlVisitor[]) { 6 | return bindVisitors(visitors)(html, { 7 | page: { 8 | path: '/', 9 | html, 10 | files: [], 11 | }, 12 | config: { 13 | assetsDir: 'assets', 14 | base: '/', 15 | bundleType: 'script', 16 | command: 'dev', 17 | defaultPath: '/404', 18 | minify: false, 19 | mode: 'development', 20 | publicDir: 'public', 21 | ssrRoutesId: '', 22 | stateCacheId: '', 23 | }, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/runtime/html/xss.ts: -------------------------------------------------------------------------------- 1 | import { MagicString } from '@utils/magic-string' 2 | import { HtmlTag } from './types' 3 | import allow from './xss/allow' 4 | 5 | /** 6 | * When the given `tag` is not allowed, `false` is returned 7 | * and the tag is removed from the given `MagicString` object. 8 | * 9 | * Otherwise, the tag's attributes are checked, and disallowed 10 | * attributes are removed. 11 | */ 12 | export function sanitizeTag(tag: HtmlTag, editor: MagicString) { 13 | const allowedAttrs = allow[tag.name] 14 | if (!allowedAttrs) { 15 | editor.remove(tag.start, tag.end) 16 | return false 17 | } 18 | for (const attr of tag.attributes) { 19 | if (!allowedAttrs.includes(attr.name.value)) { 20 | editor.remove(attr.start, attr.end) 21 | } 22 | } 23 | return true 24 | } 25 | -------------------------------------------------------------------------------- /src/runtime/http/cacheKey.ts: -------------------------------------------------------------------------------- 1 | import { murmurHash } from '@utils/murmur3' 2 | import type { Http } from './types' 3 | 4 | /** 5 | * Generate a cache key for a GET request. 6 | */ 7 | export function getCacheKey(url: string, headers?: Http.Headers) { 8 | let cacheKey = 'GET ' + url 9 | if (headers) { 10 | const keys = Object.keys(headers) 11 | if (keys.length > 1) { 12 | headers = keys.sort().reduce((sorted: any, key: string) => { 13 | sorted[key] = headers![key] 14 | return sorted 15 | }, {}) 16 | } 17 | const hash = murmurHash(JSON.stringify(headers)) 18 | cacheKey += ' ' + hash 19 | } 20 | return cacheKey 21 | } 22 | -------------------------------------------------------------------------------- /src/runtime/http/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const debug = /* @__PURE__ */ createDebug('saus:http') 4 | -------------------------------------------------------------------------------- /src/runtime/http/hooks.ts: -------------------------------------------------------------------------------- 1 | import { noop } from '@utils/noop' 2 | import { Http } from './types' 3 | 4 | export const requestHook = { current: noop as Http.RequestHook } 5 | export const responseHook = { current: noop as Http.ResponseHook } 6 | 7 | /** 8 | * Intercept a request before it's been sent. Optionally return a `Response` object. 9 | * This hook is not called if a matching response is found in the local cache. 10 | */ 11 | export function setRequestHook(onRequest: Http.RequestHook) { 12 | requestHook.current = onRequest 13 | } 14 | 15 | /** 16 | * Intercept a response before it's cached and used. The response headers can be 17 | * mutated (eg: to increase the `max-age` in the `Cache-Control` header). 18 | */ 19 | export function setResponseHook(onResponse: Http.ResponseHook) { 20 | responseHook.current = onResponse 21 | } 22 | -------------------------------------------------------------------------------- /src/runtime/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get' 2 | export * from './headers' 3 | export * from './hooks' 4 | export * from './http' 5 | export * from './httpImport' 6 | export * from './jsonImport' 7 | export * from './normalizeHeaders' 8 | export * from './redirect' 9 | export * from './response' 10 | export * from './types' 11 | export * from './unwrapBody' 12 | export * from './wrapBody' 13 | -------------------------------------------------------------------------------- /src/runtime/http/internal/urlToHttpOptions.ts: -------------------------------------------------------------------------------- 1 | import { Http, URL } from '../types' 2 | 3 | // https://github.com/nodejs/node/blob/0de6a6341a566f990d0058b28a0a3cb5b052c6b3/lib/internal/url.js#L1388 4 | export function urlToHttpOptions(url: URL) { 5 | const options: Http.Options = { 6 | protocol: url.protocol, 7 | hostname: url.hostname.startsWith('[') 8 | ? url.hostname.slice(1, -1) 9 | : url.hostname, 10 | hash: url.hash, 11 | search: url.search, 12 | pathname: url.pathname, 13 | path: `${url.pathname}${url.search}`, 14 | href: url.href, 15 | } 16 | if (url.port !== '') { 17 | options.port = Number(url.port) 18 | } 19 | if (url.username || url.password) { 20 | options.auth = `${decodeURIComponent(url.username)}:${decodeURIComponent( 21 | url.password 22 | )}` 23 | } 24 | return options 25 | } 26 | -------------------------------------------------------------------------------- /src/runtime/http/jsonImport.ts: -------------------------------------------------------------------------------- 1 | import { get } from './get' 2 | 3 | export type JsonModule = { default: any } 4 | 5 | export async function jsonImport(url: string): Promise { 6 | const json = (await get(url)).toJSON() 7 | return { default: json } 8 | } 9 | -------------------------------------------------------------------------------- /src/runtime/http/redirect.ts: -------------------------------------------------------------------------------- 1 | export class HttpRedirect { 2 | constructor(readonly location: string, readonly status = 308) {} 3 | } 4 | 5 | /** 6 | * Define an external URL to load a module or asset from. 7 | * 8 | * Pass true as the `temp` argument to use 307 for the status code. 9 | */ 10 | export function httpRedirect(location: string, temp?: boolean) { 11 | return new HttpRedirect(location, temp ? 307 : 308) 12 | } 13 | -------------------------------------------------------------------------------- /src/runtime/http/response.ts: -------------------------------------------------------------------------------- 1 | import type { Buffer } from '@utils/buffer' 2 | import { normalizeHeaders } from './normalizeHeaders' 3 | import { Http } from './types' 4 | 5 | /** 6 | * An HTTP response received from the server. 7 | */ 8 | export class HttpResponse { 9 | readonly headers: Http.ResponseHeaders 10 | readonly ok: boolean 11 | 12 | constructor( 13 | readonly data: Buffer, 14 | readonly status: number, 15 | headers: Http.ResponseHeaders 16 | ) { 17 | this.headers = normalizeHeaders(headers) 18 | this.ok = status >= 200 && status < 400 19 | } 20 | 21 | toString(encoding?: string) { 22 | return this.data.toString(encoding) 23 | } 24 | 25 | toJSON(): T { 26 | return JSON.parse(this.toString()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/runtime/http/unwrapBody.ts: -------------------------------------------------------------------------------- 1 | import { unwrapBuffer } from '@utils/node/buffer' 2 | import { AnyBody } from './writeBody' 3 | 4 | export function unwrapBody(body: AnyBody) { 5 | return body.stream 6 | ? body.stream 7 | : body.buffer 8 | ? unwrapBuffer(body.buffer) 9 | : body.text !== undefined 10 | ? body.text 11 | : body.json !== undefined 12 | ? JSON.stringify(body.json) 13 | : undefined 14 | } 15 | -------------------------------------------------------------------------------- /src/runtime/http/writeBody.ts: -------------------------------------------------------------------------------- 1 | import { Buffer } from '@utils/buffer' 2 | import { Simplify, UnionToIntersection } from 'type-fest' 3 | import { unwrapBody } from './unwrapBody' 4 | 5 | export type Body = 6 | | { buffer: Buffer } 7 | | { stream: NodeJS.ReadableStream } 8 | | { text: string; mime?: string } 9 | | { json?: any } 10 | 11 | export type AnyBody = Simplify>> 12 | 13 | export function writeBody(res: NodeJS.WritableStream, body: AnyBody) { 14 | if (body.stream) { 15 | body.stream.pipe(res, { end: true }) 16 | } else { 17 | const rawBody: any = unwrapBody(body) 18 | if (rawBody !== null) { 19 | res.write(rawBody) 20 | } 21 | res.end() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/runtime/http/writeHeaders.ts: -------------------------------------------------------------------------------- 1 | type ResponseLike = { setHeader(name: string, value: string | string[]): void } 2 | 3 | export function writeHeaders( 4 | res: ResponseLike, 5 | headers: Record 6 | ) { 7 | for (const name in headers) { 8 | const value = headers[name] 9 | if (value !== undefined) { 10 | res.setHeader(name, value) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/runtime/mapStateModule.ts: -------------------------------------------------------------------------------- 1 | import type { StateModule } from './stateModules' 2 | 3 | /** 4 | * Wrap a state module with a mapping function that 5 | * transforms the result 6 | */ 7 | export function mapStateModule( 8 | module: StateModule, 9 | map: (state: T) => U 10 | ): StateModule { 11 | let lastInput: T 12 | let lastOutput: U 13 | 14 | const clone: StateModule = Object.create( 15 | Object.getOwnPropertyDescriptors(module) 16 | ) 17 | clone.get = () => { 18 | const input = module.get() 19 | return input !== lastInput 20 | ? (lastOutput = map((lastInput = input))) 21 | : lastOutput 22 | } 23 | clone.load = () => { 24 | return module.load().then(input => { 25 | return (lastOutput = map((lastInput = input))) 26 | }) 27 | } 28 | return clone 29 | } 30 | -------------------------------------------------------------------------------- /src/runtime/renderRoutePath.ts: -------------------------------------------------------------------------------- 1 | import { InferRouteParams } from './routeTypes' 2 | 3 | const paramRegex = /(?:\/|^)([:*][^/]*?)(\?)?(?=[/.]|$)/g 4 | 5 | export const renderRoutePath = ( 6 | route: RoutePath, 7 | params: InferRouteParams 8 | ) => 9 | route.replace(paramRegex, (x, key, optional) => { 10 | x = (params as any)[key == '*' ? 'wild' : key.substring(1)] 11 | return x ? '/' + x : optional || key == '*' ? '' : '/' + key 12 | }) 13 | -------------------------------------------------------------------------------- /src/runtime/requestMetadata.ts: -------------------------------------------------------------------------------- 1 | import type { RenderedPage } from './app/types' 2 | import type { Endpoint } from './endpoint' 3 | 4 | /** 5 | * Custom metadata about an `Endpoint.Request` object 6 | */ 7 | export interface RequestMetadata { 8 | page?: RenderedPage 9 | } 10 | 11 | const metadataCache = new WeakMap, RequestMetadata>() 12 | 13 | export function getRequestMetadata(req: Endpoint.Request) { 14 | return metadataCache.get(req)! 15 | } 16 | 17 | export function setRequestMetadata( 18 | req: Endpoint.Request, 19 | data: RequestMetadata 20 | ) { 21 | metadataCache.set(req, { 22 | ...metadataCache.get(req), 23 | ...data, 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/runtime/routePlugins.ts: -------------------------------------------------------------------------------- 1 | import { Promisable } from 'type-fest' 2 | import { Route, RouteConfig, RouteModule } from './routeTypes' 3 | 4 | export type RoutePlugin< 5 | Module extends object = any, 6 | Params extends object = any 7 | > = ( 8 | config: RouteConfig 9 | ) => RoutePlugin.PostHook | void 10 | 11 | export namespace RoutePlugin { 12 | export type PostHook< 13 | Module extends object = any, 14 | Params extends object = any 15 | > = ( 16 | router: Route.API, 17 | route: Route 18 | ) => Promisable 19 | } 20 | 21 | export function defineRoutePlugin< 22 | Module extends object = RouteModule, 23 | Params extends object = {} 24 | >(plugin: RoutePlugin) { 25 | return plugin 26 | } 27 | -------------------------------------------------------------------------------- /src/runtime/routes/matchRoute.ts: -------------------------------------------------------------------------------- 1 | import { ParsedRoute } from '../routeTypes' 2 | 3 | export function matchRoute(path: string, route: ParsedRoute) { 4 | return route.pattern 5 | .exec(path) 6 | ?.slice(1) 7 | .reduce((params: Record, value, i) => { 8 | params[route.keys[i]] = value 9 | return params 10 | }, {}) 11 | } 12 | -------------------------------------------------------------------------------- /src/runtime/setLayout.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alloc/saus/dd6d90c7ae15ba98f026e044e35f51357e2a7546/src/runtime/setLayout.ts -------------------------------------------------------------------------------- /src/runtime/setup.ts: -------------------------------------------------------------------------------- 1 | import type { RuntimeHook } from './config' 2 | import { routesModule } from './global' 3 | 4 | /** 5 | * Set up the runtime according to the given environment. 6 | * 7 | * Can only be called from your `saus.routes` module or from 8 | * a module imported by it (directly or transiently). 9 | */ 10 | export function setup(hook: RuntimeHook) { 11 | routesModule.runtimeHooks.push(hook) 12 | } 13 | -------------------------------------------------------------------------------- /src/runtime/stateModules/README.md: -------------------------------------------------------------------------------- 1 | Modules in this folder are replaced for the client bundle. 2 | -------------------------------------------------------------------------------- /src/runtime/stateModules/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const debug = createDebug('saus:state') 4 | -------------------------------------------------------------------------------- /src/runtime/stateModules/get.ts: -------------------------------------------------------------------------------- 1 | import { klona as deepCopy } from '@utils/klona' 2 | import { Cache } from '../cache/types' 3 | import { getLoadedStateOrThrow } from '../getLoadedStateOrThrow' 4 | import { getStateModuleKey } from '../getStateModuleKey' 5 | import type { StateModule } from '../stateModules' 6 | 7 | /** 8 | * Unwrap a state module with the given arguments. \ 9 | * Throws an error when the state isn't already loaded. 10 | */ 11 | export function getState( 12 | cache: Cache, 13 | module: StateModule, 14 | args: Args 15 | ) { 16 | const key = getStateModuleKey(module.name, args) 17 | const cached = getLoadedStateOrThrow(cache, key, args) 18 | return deepCopy(cached) as Cache.Entry 19 | } 20 | -------------------------------------------------------------------------------- /src/runtime/stateModules/global.ts: -------------------------------------------------------------------------------- 1 | import { getStackFrame } from '@utils/node/stack/getStackFrame' 2 | import { stateModulesByName } from '../cache/global' 3 | import type { StateModule } from '../stateModules' 4 | 5 | export const stateModulesByFile = new Map>() 6 | 7 | export function trackStateModule(module: StateModule) { 8 | // Skip this function and the `defineStateModule` function. 9 | const caller = getStackFrame(2) 10 | if (caller) { 11 | const modules = stateModulesByFile.get(caller.file) || new Map() 12 | stateModulesByFile.set(caller.file, modules) 13 | modules.set(module.name, module) 14 | } 15 | stateModulesByName.set(module.name, module) 16 | } 17 | -------------------------------------------------------------------------------- /src/runtime/stateModules/setState.ts: -------------------------------------------------------------------------------- 1 | import { stateModulesByName } from '../cache/global' 2 | import type { Cache } from '../cache/types' 3 | import { getStateModuleKey } from '../getStateModuleKey' 4 | import { hydrateState } from '../stateModules/hydrate' 5 | 6 | /** 7 | * State modules must call this when loaded by the client. 8 | */ 9 | export function setState( 10 | name: string, 11 | args: Args, 12 | state: any, 13 | timestamp: number, 14 | maxAge?: Cache.MaxAge 15 | ): any { 16 | const key = getStateModuleKey(name, args) 17 | const served = { state, args, timestamp, maxAge } 18 | const module = stateModulesByName.get(name) 19 | if (module) { 20 | hydrateState(key, served, module) 21 | } 22 | return state 23 | } 24 | -------------------------------------------------------------------------------- /src/runtime/tokens.ts: -------------------------------------------------------------------------------- 1 | import { compact } from './constants' 2 | 3 | export const SPACE = compact ? '' : ' ' 4 | export const RETURN = compact ? '' : '\n' 5 | export const INDENT = compact ? '' : ' ' 6 | -------------------------------------------------------------------------------- /src/runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "exclude": ["**/*.spec.ts", "dist", "tsup.config.ts"], 5 | "compilerOptions": { 6 | "composite": false, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "incremental": true, 10 | "moduleResolution": "node", 11 | "noEmitOnError": false, 12 | "outDir": "dist", 13 | "tsBuildInfoFile": "dist/.tsbuildinfo" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/runtime/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions' 2 | import { defineConfig } from 'tsup' 3 | 4 | export default defineConfig({ 5 | entry: [ 6 | '**/*.ts', 7 | '!**/*.spec.ts', 8 | '!*.config.ts', 9 | '!node_modules/**', 10 | '!dist/**', 11 | ], 12 | outDir: 'dist', 13 | format: ['cjs', 'esm'], 14 | bundle: false, 15 | plugins: [esbuildPluginFilePathExtensions()], 16 | }) 17 | -------------------------------------------------------------------------------- /src/secrets/api.ts: -------------------------------------------------------------------------------- 1 | export { loadDeployContext } from '../deploy/context' 2 | export { loadSecretSources } from './loadSecretSources' 3 | export { selectSource } from './utils/selectSource' 4 | -------------------------------------------------------------------------------- /src/secrets/defineSecrets.ts: -------------------------------------------------------------------------------- 1 | import { kSecretDefinition } from './symbols' 2 | import { DefinedSecrets, SecretMap } from './types' 3 | 4 | export function defineSecrets(def: T): DefinedSecrets { 5 | return { [kSecretDefinition]: def } as any 6 | } 7 | -------------------------------------------------------------------------------- /src/secrets/loadSecretSources.ts: -------------------------------------------------------------------------------- 1 | import { defer } from '@utils/defer' 2 | import { noop } from '@utils/noop' 3 | import { green } from 'kleur/colors' 4 | import { DeployContext } from '../deploy/context' 5 | import { loadDeployFile } from '../deploy/loader' 6 | 7 | export async function loadSecretSources(context: DeployContext) { 8 | // Use the `addTarget` function to detect when to 9 | // start loading from our secret sources. 10 | const loading = defer() 11 | context.addDeployTarget = () => loading.resolve() 12 | context.addDeployAction = () => { 13 | loading.resolve() 14 | return new Promise(noop) 15 | } 16 | 17 | loadDeployFile(context) 18 | await loading.promise 19 | 20 | if (context.logger.isLogged('info')) { 21 | process.stderr.write(green('✔︎') + ' Plugin secrets were loaded.\n') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/secrets/prompt.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from '@utils/node/prompt' 2 | import { bold } from 'kleur/colors' 3 | import { SecretMap } from './types' 4 | 5 | export async function askForSecrets(names: Iterable) { 6 | const secrets: SecretMap = {} 7 | for (const name of names) { 8 | console.log(`\nWhat should ${bold(name)} be?`) 9 | let aborted = false 10 | const { secret } = await prompt( 11 | { 12 | name: 'secret', 13 | type: 'password', 14 | message: '', 15 | }, 16 | { 17 | onCancel() { 18 | aborted = true 19 | }, 20 | } 21 | ) 22 | if (aborted) { 23 | break 24 | } 25 | if (secret) { 26 | secrets[name] = secret 27 | } 28 | } 29 | if (Object.keys(secrets).length) { 30 | return secrets 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/secrets/runtime/checkSecrets.ts: -------------------------------------------------------------------------------- 1 | import { getDeployContext } from '../../deploy/context' 2 | 3 | /** 4 | * ⚠︎ Never call this function manually! 5 | */ 6 | export function checkSecrets(importedValues: any[]) { 7 | const { secrets } = getDeployContext() 8 | for (const value of importedValues) { 9 | if (typeof value == 'function') { 10 | secrets['_imported'].add(value) 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/secrets/runtime/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../types' 2 | export * from './addSecrets' 3 | export * from './checkSecrets' 4 | -------------------------------------------------------------------------------- /src/secrets/symbols.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This symbol is used to store the secret->alias mapping on 3 | * each `defineSecrets` result. 4 | */ 5 | export const kSecretDefinition = Symbol.for('saus.SecretDefinition') 6 | -------------------------------------------------------------------------------- /src/secrets/utils/selectSource.ts: -------------------------------------------------------------------------------- 1 | import { prompt } from '@utils/node/prompt' 2 | import { MutableSecretSource } from '../types' 3 | 4 | export async function selectSource( 5 | sources: MutableSecretSource[] 6 | ): Promise { 7 | if (!sources.length) { 8 | throw Error('[saus] None of your deploy plugins allow editing secrets.') 9 | } 10 | if (sources.length > 1) { 11 | const selection = await prompt({ 12 | name: 'source', 13 | type: 'select', 14 | choices: sources.map(s => ({ title: s.name, value: s })), 15 | }) 16 | return selection?.source 17 | } 18 | return sources[0] 19 | } 20 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts"], 3 | "exclude": [ 4 | "**/*.spec.ts", 5 | "*.config.ts", 6 | "dist", 7 | "client", 8 | "runtime", 9 | "utils", 10 | "vm" 11 | ], 12 | "compilerOptions": { 13 | "declaration": true, 14 | "declarationMap": true, 15 | "esModuleInterop": true, 16 | "incremental": true, 17 | "lib": ["es2019"], 18 | "module": "commonjs", 19 | "moduleResolution": "node", 20 | "noUnusedLocals": true, 21 | "outDir": "dist", 22 | "paths": { 23 | "@/*": ["./core/*"] 24 | }, 25 | "skipLibCheck": true, 26 | "strict": true, 27 | "sourceMap": true, 28 | "target": "es2019", 29 | "tsBuildInfoFile": "dist/.tsbuildinfo" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/AbortController/index.ts: -------------------------------------------------------------------------------- 1 | export type AbortController = import('./types').AbortController 2 | export const AbortController = 3 | globalThis.AbortController as typeof import('./types').AbortController 4 | 5 | export type AbortSignal = import('./types').AbortSignal 6 | export const AbortSignal = 7 | globalThis.AbortSignal as typeof import('./types').AbortSignal 8 | -------------------------------------------------------------------------------- /src/utils/__snapshots__/esmToCjs.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`esmToCjs > named exports > rewrites bindings 1`] = `"exports.foo = 1"`; 4 | 5 | exports[`esmToCjs > named exports > rewrites bindings 2`] = `"exports.foo = 2"`; 6 | 7 | exports[`esmToCjs > named exports > rewrites classes 1`] = `"exports.Foo = class Foo {}"`; 8 | 9 | exports[`esmToCjs > named exports > rewrites functions 1`] = `"exports.foo = function foo() {}"`; 10 | 11 | exports[`esmToCjs > named exports > rewrites identifiers 1`] = ` 12 | "const Foo = 1, Bar = 2 13 | exports.Foo = Foo 14 | exports.default = Bar" 15 | `; 16 | -------------------------------------------------------------------------------- /src/utils/array.ts: -------------------------------------------------------------------------------- 1 | import { Falsy } from './types' 2 | 3 | /** 4 | * Convert `arg` to an array and ensure the returned array 5 | * never contains `undefined` values. 6 | */ 7 | export const toArray = ( 8 | arg: T 9 | ): (T extends readonly (infer U)[] 10 | ? Exclude 11 | : Exclude)[] => 12 | arg !== undefined 13 | ? Array.isArray(arg) 14 | ? arg.filter(value => value !== undefined) 15 | : ([arg] as any) 16 | : [] 17 | 18 | export function mergeArrays(...arrays: (readonly T[] | Falsy)[]) { 19 | return arrays.filter(Boolean).flat() as T[] 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/ascendBranch.ts: -------------------------------------------------------------------------------- 1 | type BranchProp = keyof T extends infer Key 2 | ? Key extends string & keyof T 3 | ? T[Key] extends T | undefined 4 | ? Key 5 | : never 6 | : never 7 | : never 8 | 9 | export function ascendBranch( 10 | node: T | undefined, 11 | parentKey: BranchProp, 12 | iterator: (node: T) => U 13 | ): U[] { 14 | const rets: U[] = [] 15 | while (node) { 16 | rets.push(iterator(node)) 17 | node = node[parentKey] as any 18 | } 19 | return rets 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/assignDefaults.ts: -------------------------------------------------------------------------------- 1 | import {} from 'type-fest' 2 | 3 | export function assignDefaults>( 4 | target: T, 5 | defaults: U 6 | ): T & U 7 | 8 | export function assignDefaults(target: any, defaults: any) { 9 | for (const key in defaults) { 10 | if (target[key] === undefined) { 11 | target[key] = defaults[key] 12 | } 13 | } 14 | return target 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/babel.ts: -------------------------------------------------------------------------------- 1 | import * as babel from '@babel/core' 2 | import { NodePath, types as t } from '@babel/core' 3 | 4 | export { getBabelConfig } from './babel/config' 5 | export { getBabelProgram } from './babel/program' 6 | export { transformAsync, transformSync } from './babel/transform' 7 | export { NodePath, t, babel } 8 | -------------------------------------------------------------------------------- /src/utils/babel/program.ts: -------------------------------------------------------------------------------- 1 | import { NodePath, types as t } from '@babel/core' 2 | import { transformSync } from './transform' 3 | 4 | export function getBabelProgram(source: string, filename: string) { 5 | let program: NodePath | undefined 6 | const visitor: babel.Visitor = { 7 | Program: path => { 8 | program = path 9 | path.stop() 10 | }, 11 | } 12 | 13 | // Use the AST for traversal, but not code generation. 14 | transformSync(source, filename, { 15 | plugins: [{ visitor }], 16 | sourceMaps: false, 17 | sourceType: 'module', 18 | code: false, 19 | }) 20 | 21 | return program! 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/base.ts: -------------------------------------------------------------------------------- 1 | const trailingSlash = /\/$/ 2 | 3 | export function prependBase(uri: string, base: string) { 4 | return base.replace(trailingSlash, uri[0] === '/' ? uri : '/' + uri) 5 | } 6 | 7 | export function baseToRegex(base: string) { 8 | return new RegExp('^' + base.replace(/\./g, '\\.')) 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/buffer.ts: -------------------------------------------------------------------------------- 1 | import { TextDecoder } from './node/textDecoder' 2 | 3 | /** 4 | * Simple wrapper around TextDecoder to help provide HTTP helpers that 5 | * work in all environments. 6 | */ 7 | export class Buffer { 8 | // Note: This class is replaced in Node environments. 9 | protected constructor(readonly buffer: ArrayBuffer) {} 10 | 11 | get length() { 12 | return this.buffer.byteLength 13 | } 14 | 15 | static isBuffer(buf: any): buf is Buffer { 16 | return buf instanceof Buffer 17 | } 18 | 19 | static from(data: ArrayBuffer) { 20 | return new Buffer(data) 21 | } 22 | 23 | toString(encoding?: string): string { 24 | const decoder = new TextDecoder(encoding) 25 | return decoder.decode(this.buffer) 26 | } 27 | } 28 | 29 | export function unwrapBuffer(buf: Buffer) { 30 | return buf // Client buffers remain unchanged. 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/callPlugins.ts: -------------------------------------------------------------------------------- 1 | type AnyFn = (...args: any[]) => any 2 | type MethodOf = { 3 | [P in keyof T]: T[P] extends AnyFn | undefined ? P : never 4 | }[keyof T] 5 | 6 | export async function callPlugins>( 7 | plugins: readonly T[], 8 | method: P, 9 | ...args: Parameters> 10 | ): Promise { 11 | for (const plugin of plugins) { 12 | if (typeof plugin[method] == 'function') { 13 | await (plugin[method] as any)(...args) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/cleanUrl.ts: -------------------------------------------------------------------------------- 1 | const urlToPathRE = /[#?].+$/ 2 | 3 | export const cleanUrl = (url: string): string => 4 | url.replace(urlToPathRE, '') 5 | -------------------------------------------------------------------------------- /src/utils/dedupe.ts: -------------------------------------------------------------------------------- 1 | export function dedupe( 2 | arr: Iterable | readonly T[], 3 | mapper?: (value: T, index: number) => U 4 | ): U[] { 5 | return Array.from(new Set(arr) as any, mapper!) 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/defer.ts: -------------------------------------------------------------------------------- 1 | export type Deferred = PromiseLike & { 2 | resolve: undefined extends T 3 | ? (value?: T | PromiseLike) => void 4 | : (value: T | PromiseLike) => void 5 | reject: (error?: any) => void 6 | promise: Promise 7 | settled: boolean 8 | } 9 | 10 | export function defer() { 11 | const result = {} as Deferred 12 | const promise = new Promise((resolve, reject) => { 13 | result.resolve = resolve as any 14 | result.reject = reject 15 | }) 16 | promise.finally(() => { 17 | result.settled = true 18 | }) 19 | result.then = promise.then.bind(promise) as any 20 | result.promise = promise 21 | return result 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/defineLazy.ts: -------------------------------------------------------------------------------- 1 | export function defineLazy(obj: T, props: { [P in keyof T]?: () => T[P] }) { 2 | for (const [key, get] of Object.entries(props)) 3 | Object.defineProperty(obj, key, { 4 | enumerable: true, 5 | configurable: true, 6 | get() { 7 | const value = get() 8 | Object.defineProperty(obj, key, { 9 | value, 10 | enumerable: true, 11 | configurable: true, 12 | }) 13 | return value 14 | }, 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "exports": { 3 | "./AbortController": { 4 | "node": "./AbortController/index.node.js", 5 | "types": "./AbortController/index.d.ts", 6 | "import": "./AbortController/index.mjs", 7 | "default": "./AbortController/index.js" 8 | }, 9 | "./*": { 10 | "types": "./*.d.ts", 11 | "import": "./*.mjs", 12 | "default": "./*.js" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/escape.ts: -------------------------------------------------------------------------------- 1 | // Adapted from the `escape-goat` package. 2 | 3 | export function escape( 4 | strings: string | TemplateStringsArray, 5 | ...values: any[] 6 | ) { 7 | if (typeof strings === 'string') { 8 | return escapeHtml(strings) 9 | } 10 | 11 | let output = strings[0] 12 | for (const [index, value] of values.entries()) { 13 | output = output + escapeHtml(String(value)) + strings[index + 1] 14 | } 15 | 16 | return output 17 | } 18 | 19 | // Multiple `.replace()` calls are actually faster than using replacer functions. 20 | const escapeHtml = (html: string) => 21 | html 22 | .replace(/&/g, '&') // Must happen first or else it will escape other just-escaped characters. 23 | .replace(/"/g, '"') 24 | .replace(/'/g, ''') 25 | .replace(//g, '>') 27 | -------------------------------------------------------------------------------- /src/utils/generateId.ts: -------------------------------------------------------------------------------- 1 | export function generateId() { 2 | return Math.random().toString(36).substring(2, 10) 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/getPageFilename.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the `.html` filename for a given URL pathname. 3 | * 4 | * Beware: Trailing slashes are treated as `/index.html` and querystrings 5 | * are not supported. 6 | */ 7 | export function getPageFilename(path: string, basePath?: string) { 8 | if (basePath && new RegExp('^' + basePath + '?$').test(path)) { 9 | return basePath.slice(1) + 'index.html' 10 | } 11 | return path.replace(/(?:\/(index)?)?$/, appendHtmlSuffix).replace(/^\//, '') 12 | } 13 | 14 | function appendHtmlSuffix(indexSuffix?: string, indexPath?: string) { 15 | return ( 16 | (indexPath ? 'index/' : '') + 17 | (indexSuffix ? (indexPath ? '' : '/') + 'index' : '') + 18 | '.html' 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/httpMethods.ts: -------------------------------------------------------------------------------- 1 | export const httpMethods = [ 2 | 'get', 3 | 'post', 4 | 'put', 5 | 'patch', 6 | 'delete', 7 | 'head', 8 | ] as const 9 | -------------------------------------------------------------------------------- /src/utils/importRegex.ts: -------------------------------------------------------------------------------- 1 | export const bareImportRE = /^[\w@][^:]/ 2 | export const relativePathRE = /^(?:\.\/|(\.\.\/)+)/ 3 | 4 | /** Similar to `relativePathRE` but it matches `../` prefix too. */ 5 | export const internalPathRE = /^(?:[.@]\/|(\.\.\/)+)/ 6 | -------------------------------------------------------------------------------- /src/utils/isCSSRequest.ts: -------------------------------------------------------------------------------- 1 | const cssLangs = `\\.(css|less|sass|scss|styl|stylus|pcss|postcss)($|\\?)` 2 | const cssLangRE = new RegExp(cssLangs) 3 | 4 | export const isCSSRequest = (request: string): boolean => 5 | cssLangRE.test(request) 6 | -------------------------------------------------------------------------------- /src/utils/isExternalUrl.ts: -------------------------------------------------------------------------------- 1 | const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/ 2 | 3 | const localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/ 4 | const nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/ 5 | 6 | export function isExternalUrl(url: string) { 7 | if (typeof url !== 'string') { 8 | return false 9 | } 10 | 11 | const match = url.match(protocolAndDomainRE) 12 | if (!match) { 13 | return false 14 | } 15 | 16 | const everythingAfterProtocol = match[1] 17 | if (!everythingAfterProtocol) { 18 | return false 19 | } 20 | 21 | if (localhostDomainRE.test(everythingAfterProtocol)) { 22 | return false 23 | } 24 | 25 | if (nonLocalhostDomainRE.test(everythingAfterProtocol)) { 26 | return true 27 | } 28 | 29 | return false 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/isObject.ts: -------------------------------------------------------------------------------- 1 | export function isObject(o: any): o is object { 2 | return !!o && typeof o == 'object' && !Array.isArray(o) 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/isPackageRef.ts: -------------------------------------------------------------------------------- 1 | const packageRefRE = /^(\w|@\w)/ 2 | 3 | /** 4 | * Assume bare imports are referencing another package, unless 5 | * they start with "../" (a common alias for local files). 6 | * 7 | * Note: This function assumes you're passing a bare import! 8 | */ 9 | export const isPackageRef = (id: string) => packageRefRE.test(id) 10 | -------------------------------------------------------------------------------- /src/utils/joinUrl.ts: -------------------------------------------------------------------------------- 1 | export function joinUrl(...parts: (string | undefined)[]) { 2 | parts = parts.filter(Boolean) 3 | const head = parts[0] || '' 4 | const protocol = /^[^./]+:\/\//.exec(head) 5 | if (protocol) { 6 | parts[0] = head.slice(protocol[0].length) 7 | } 8 | const path = ('/' + parts.join('/')).replace(/\/{2,}/g, '/') 9 | return protocol ? protocol[0] + path.slice(1) : path 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/klona.ts: -------------------------------------------------------------------------------- 1 | export function klona(val: T, objects = new Map()): T { 2 | let out = objects.get(val) 3 | if (out) { 4 | return out 5 | } 6 | 7 | let k: any 8 | let tmp: any 9 | 10 | if (Array.isArray(val)) { 11 | out = Array((k = val.length)) 12 | objects.set(val, out) 13 | while (k--) { 14 | out[k] = 15 | (tmp = val[k]) && typeof tmp === 'object' ? klona(tmp, objects) : tmp 16 | } 17 | return out 18 | } 19 | 20 | if (Object.prototype.toString.call(val) === '[object Object]') { 21 | out = {} 22 | objects.set(val, out) 23 | for (k in val) { 24 | if (k !== '__proto__') { 25 | out[k] = 26 | (tmp = val[k as keyof T]) && typeof tmp === 'object' 27 | ? klona(tmp, objects) 28 | : tmp 29 | } 30 | } 31 | return out 32 | } 33 | 34 | return val 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/limitTime.ts: -------------------------------------------------------------------------------- 1 | declare const globalThis: any 2 | 3 | type Promisable = T | PromiseLike 4 | 5 | export function limitTime( 6 | promise: Promisable, 7 | secs: number, 8 | reason?: string 9 | ): Promise { 10 | if (!(promise instanceof Promise)) { 11 | return Promise.resolve(promise) 12 | } 13 | if (secs <= 0 || globalThis.__inspectorActive) { 14 | return promise 15 | } 16 | const trace = Error(reason || 'Timed out') 17 | return new Promise((resolve, reject) => { 18 | const id = setTimeout(() => reject(trace), secs * 1e3) 19 | promise.then(resolve, reject).finally(() => clearTimeout(id)) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/magic-string.ts: -------------------------------------------------------------------------------- 1 | export { Bundle as MagicBundle, default as MagicString } from 'magic-string' 2 | export type { 3 | BundleOptions as MagicBundleOptions, 4 | MagicStringOptions, 5 | } from 'magic-string' 6 | -------------------------------------------------------------------------------- /src/utils/mapSerial.ts: -------------------------------------------------------------------------------- 1 | type Promisable = T | PromiseLike 2 | 3 | export async function mapSerial( 4 | array: readonly T[], 5 | mapper: (element: T) => Promisable 6 | ): Promise { 7 | const mapped: U[] = [] 8 | for (const element of array) { 9 | mapped.push(await mapper(element)) 10 | } 11 | return mapped 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/memoizeFn.ts: -------------------------------------------------------------------------------- 1 | export function memoizeFn( 2 | fn: T, 3 | call: (fn: T, ...args: Args) => Return 4 | ): (...args: Args) => Return { 5 | const cache = new Map() 6 | return (...args: Args) => { 7 | const cacheKey = JSON.stringify(args) 8 | if (!cache.has(cacheKey)) { 9 | const result = call(fn, ...args) 10 | cache.set(cacheKey, result) 11 | return result 12 | } 13 | return cache.get(cacheKey) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/murmur3.ts: -------------------------------------------------------------------------------- 1 | import { murmurHash as hash } from 'ohash' 2 | 3 | declare const TextEncoder: any 4 | 5 | const nullChar = '\0' 6 | const nullByte: Uint8Array = new TextEncoder().encode(nullChar) 7 | 8 | export function murmurHash(data: string | Uint8Array) { 9 | const padding = (4 - (data.length & 3)) & 3 10 | if (typeof data == 'string') { 11 | data = data.padEnd(data.length + padding, nullChar) 12 | return hash(data) 13 | } 14 | const resized = new Uint8Array(data.byteLength + padding) 15 | resized.set(data) 16 | for (let i = 0; i < padding; i++) { 17 | resized.set(nullByte, data.length + i) 18 | } 19 | return hash(resized) 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/node/ansiToHtml.ts: -------------------------------------------------------------------------------- 1 | import AnsiConverter from 'ansi-to-html' 2 | 3 | const ansiTheme = { 4 | 0: '#000000', 5 | 1: '#C75646', 6 | 2: '#8EB33B', 7 | 3: '#D0B03C', 8 | 4: '#4E90A7', 9 | 5: '#C8A0D1', 10 | 6: '#218693', 11 | 7: '#B0B0B0', 12 | 10: '#5D5D5D', 13 | 11: '#E09690', 14 | 12: '#CDEE69', 15 | 13: '#FFE377', 16 | 14: '#9CD9F0', 17 | 15: '#FBB1F9', 18 | 16: '#77DFD8', 19 | 17: '#F7F7F7', 20 | } 21 | 22 | export function ansiToHtml(input: string) { 23 | const convert = new AnsiConverter({ 24 | newline: true, 25 | colors: ansiTheme, 26 | }) 27 | return convert 28 | .toHtml(input) 29 | .replace(/\bhttps:\/\/[^\s]+/, match => `
${match}`) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/node/bindExec.ts: -------------------------------------------------------------------------------- 1 | import exec from '@cush/exec' 2 | 3 | export function bindExec(...boundArgs: exec.Args): typeof exec.async 4 | 5 | export function bindExec( 6 | cmd: string, 7 | ...boundArgs: exec.Args 8 | ): typeof exec.async 9 | 10 | export function bindExec(...boundArgs: any[]) { 11 | const boundCmd = typeof boundArgs[0] == 'string' ? boundArgs.shift() : '' 12 | return (cmd: string, ...args: exec.Args) => 13 | exec((boundCmd ? boundCmd + ' ' : '') + cmd, ...args, ...boundArgs) 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/node/currentModule.ts: -------------------------------------------------------------------------------- 1 | import { getStackFrame } from './stack/getStackFrame' 2 | 3 | /** 4 | * Used by the `route` function to resolve the entry module ID of a 5 | * generated route relative to the caller. 6 | * 7 | * In SSR bundles, this function is swapped out for an implementation 8 | * that introspects the in-memory SSR module system. 9 | */ 10 | export const getCurrentModule = () => 11 | // Skip this function and its caller. 12 | getStackFrame(2)?.file 13 | -------------------------------------------------------------------------------- /src/utils/node/emptyDir.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs' 3 | 4 | /** 5 | * Delete every file and subdirectory. **The given directory must exist.** 6 | * Pass an optional `skip` array to preserve files in the root directory. 7 | */ 8 | export function emptyDir(dir: string, skip?: string[]): void { 9 | for (const file of fs.readdirSync(dir)) { 10 | if (skip?.includes(file)) { 11 | continue 12 | } 13 | const abs = path.resolve(dir, file) 14 | // baseline is Node 12 so can't use rmSync :( 15 | if (fs.lstatSync(abs).isDirectory()) { 16 | emptyDir(abs) 17 | fs.rmdirSync(abs) 18 | } else { 19 | fs.unlinkSync(abs) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/node/findPackage.ts: -------------------------------------------------------------------------------- 1 | import escalade from 'escalade/sync' 2 | import { statSync } from 'fs' 3 | import { dirname } from 'path' 4 | 5 | export function findPackage(fromDir: string, tolerateMissingPaths?: boolean) { 6 | while (tolerateMissingPaths) { 7 | try { 8 | statSync(fromDir) 9 | break 10 | } catch (e: any) { 11 | if (e.code !== 'ENOENT') { 12 | throw e 13 | } 14 | fromDir = dirname(fromDir) 15 | } 16 | } 17 | return escalade(fromDir, (_parent, children) => { 18 | return children.find(name => name == 'package.json') 19 | }) as string | undefined 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/node/getRawGitHubUrl.ts: -------------------------------------------------------------------------------- 1 | export const getRawGitHubUrl = (opts: { 2 | token: string 3 | repo: string 4 | branch: string 5 | file: string 6 | }) => 7 | `https://${opts.token}@raw.githubusercontent.com/${opts.repo}/${opts.branch}/${opts.file}` 8 | -------------------------------------------------------------------------------- /src/utils/node/git/createCommit.ts: -------------------------------------------------------------------------------- 1 | import exec from '@cush/exec' 2 | 3 | const nothingToCommitRE = /\bnothing\b.*? to commit\b/ 4 | 5 | /** 6 | * Commit the staged changes in a repository, ignoring non-zero exit codes 7 | * when “nothing to commit” is found in the output logs. 8 | * 9 | * Not available in a production SSR context. 10 | */ 11 | export function createCommit(message: string, ...args: exec.SyncArgs) { 12 | const stdout = exec.sync(`git commit -m`, [message], ...args, { 13 | noThrow: nothingToCommitRE, 14 | }) 15 | return { 16 | stdout, 17 | success: !nothingToCommitRE.test(stdout), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/node/lazyImport.ts: -------------------------------------------------------------------------------- 1 | export const lazyImport: (id: string) => Promise = (0, eval)( 2 | 'id => import(id)' 3 | ) 4 | -------------------------------------------------------------------------------- /src/utils/node/limitConcurrency.ts: -------------------------------------------------------------------------------- 1 | import os from 'os' 2 | import { ExecutionGateContext } from '../controlExecution' 3 | 4 | const cpuCount = os.cpus().length 5 | 6 | /** 7 | * Prevent too many active calls at one time. 8 | * 9 | * If `limit` is null or undefined, then `os.cpus().length` is used. 10 | */ 11 | export function limitConcurrency(limit?: number | null) { 12 | const maxConcurrency = limit == null ? cpuCount : limit 13 | return (ctx: ExecutionGateContext, wasQueued?: boolean) => { 14 | const availableCalls = maxConcurrency - ctx.activeCalls.size 15 | if (!wasQueued && ctx.queuedCalls.length >= availableCalls) { 16 | return false 17 | } 18 | return availableCalls > 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/node/prompt.ts: -------------------------------------------------------------------------------- 1 | import prompt, { 2 | Answers, 3 | Choice, 4 | Options as PromptOptions, 5 | PromptObject, 6 | PromptType, 7 | } from 'prompts' 8 | 9 | export { prompt, Answers, Choice, PromptOptions, PromptObject, PromptType } 10 | -------------------------------------------------------------------------------- /src/utils/node/relativeToCwd.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export function relativeToCwd(file: string) { 4 | if (!path.isAbsolute(file)) { 5 | return file 6 | } 7 | file = path.relative(process.cwd(), file) 8 | return file.startsWith('../') ? file : './' + file 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/node/servedPathForFile.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | export function servedPathForFile(id: string, root: string, exists?: boolean) { 4 | if (id[0] === '\0' || id.startsWith('/@fs/')) { 5 | return id 6 | } 7 | if (id.startsWith(root + '/')) { 8 | return id.slice(root.length) 9 | } 10 | exists ??= fs.existsSync(id) 11 | return exists ? '/@fs/' + id : id 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/node/shortcuts.ts: -------------------------------------------------------------------------------- 1 | export function onShortcut( 2 | stdin: NodeJS.ReadStream, 3 | handler: (key: string, resume: () => void) => void 4 | ): void { 5 | stdin 6 | .setRawMode(true) 7 | .setEncoding('utf8') 8 | .once('data', data => { 9 | stdin.pause() 10 | handler(String(data || ''), () => { 11 | onShortcut(stdin, handler) 12 | }) 13 | }) 14 | .resume() 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/node/stack/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../../parseStackTrace' 2 | export * from './getStackFrame' 3 | export * from './resolveStackTrace' 4 | export * from './traceStackFrame' 5 | -------------------------------------------------------------------------------- /src/utils/node/stack/resolveStackTrace.ts: -------------------------------------------------------------------------------- 1 | import { parseStackTrace } from '../../parseStackTrace' 2 | import { SourceMap } from '../sourceMap' 3 | import { traceStackFrame } from './traceStackFrame' 4 | 5 | export function resolveStackTrace( 6 | stack: string, 7 | getSourceMap: (file: string) => SourceMap | null 8 | ) { 9 | const parsed = parseStackTrace(stack) 10 | const lines = [parsed.header] 11 | for (const frame of parsed.frames) { 12 | const map = getSourceMap(frame.file) 13 | if (map) { 14 | traceStackFrame(frame, map) 15 | } 16 | lines.push(frame.text) 17 | } 18 | 19 | return lines.join('\n') 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/node/textDecoder.ts: -------------------------------------------------------------------------------- 1 | export { TextDecoder } from 'util' 2 | -------------------------------------------------------------------------------- /src/utils/node/tinypool.ts: -------------------------------------------------------------------------------- 1 | import Tinypool from 'tinypool' 2 | import { lazyImport } from './lazyImport' 3 | 4 | // Tinypool is ESM only, so use dynamic import to load it. 5 | export const loadTinypool = async () => 6 | ((await lazyImport('tinypool')) as typeof import('tinypool')).default 7 | 8 | export type { Tinypool } from 'tinypool' 9 | 10 | export type WorkerPool = Omit & { 11 | run

( 12 | task: [P, ...Parameters>], 13 | options?: Parameters[1] 14 | ): Promise>, void>>> 15 | } 16 | 17 | type Fn = (...args: any[]) => any 18 | -------------------------------------------------------------------------------- /src/utils/node/toDebugPath.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { relativeToCwd } from './relativeToCwd' 3 | 4 | export function toDebugPath(file: string) { 5 | return fs.existsSync(file.replace(/[#?].*$/, '')) ? relativeToCwd(file) : file 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/noop.ts: -------------------------------------------------------------------------------- 1 | export function noop() {} 2 | -------------------------------------------------------------------------------- /src/utils/objectHash.ts: -------------------------------------------------------------------------------- 1 | import { murmurHash } from './murmur3' 2 | import { sortObjects } from './sortObjects' 3 | 4 | export function toObjectHash(data: object) { 5 | const json = JSON.stringify(data, sortObjects) 6 | return murmurHash(json) 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/parseLazyImport.ts: -------------------------------------------------------------------------------- 1 | const importRE = /\b\(["']([^"']+)["']\)/ 2 | 3 | export const parseLazyImport = (fn: Function) => { 4 | const match = importRE.exec(fn.toString()) 5 | return match && match[1] 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/pick.ts: -------------------------------------------------------------------------------- 1 | import { PossibleKeys } from './types' 2 | 3 | export function pick< 4 | T extends object, 5 | P extends ReadonlyArray> 6 | >( 7 | obj: T, 8 | keys: P, 9 | filter: (value: any, key: P[number]) => boolean = () => true 10 | ): Pick { 11 | const picked: any = {} 12 | for (const key of keys) { 13 | const value = obj[key] 14 | if (filter(value, key)) { 15 | picked[key] = value 16 | } 17 | } 18 | return picked 19 | } 20 | 21 | export function pickAllExcept< 22 | T extends object, 23 | P extends ReadonlyArray> 24 | >(obj: T, keys: P) { 25 | return pick(obj, Object.keys(obj) as any, (_, key) => !keys.includes(key)) 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/plural.ts: -------------------------------------------------------------------------------- 1 | export function plural(count: number, one: string, many?: string) { 2 | return count + ' ' + (count == 1 ? one : many || one + 's') 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/readJson.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs' 2 | 3 | export type Reviver = (this: any, key: string, value: any) => any 4 | 5 | export function readJson(p: string, reviver?: Reviver): T { 6 | return JSON.parse(readFileSync(p, 'utf8'), reviver) 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/reduceSerial.ts: -------------------------------------------------------------------------------- 1 | type Promisable = T | PromiseLike 2 | 3 | export async function reduceSerial( 4 | array: readonly T[], 5 | reducer: (result: U, element: T) => Promisable, 6 | init: U 7 | ): Promise { 8 | let reduced = init 9 | for (const element of array) { 10 | const result = await reducer(reduced, element) 11 | if (result != null) { 12 | reduced = result 13 | } 14 | } 15 | return reduced 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/resolveModules.ts: -------------------------------------------------------------------------------- 1 | import { unwrapDefault } from './unwrapDefault' 2 | 3 | export async function resolveModules[]>( 4 | ...modules: T 5 | ): Promise<{ 6 | [Index in keyof T]: Awaited extends infer Resolved 7 | ? Resolved extends { default: infer DefaultExport } 8 | ? DefaultExport 9 | : Resolved 10 | : never 11 | }> { 12 | return (await Promise.all(modules)).map(unwrapDefault) as any 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/rollupTypes.ts: -------------------------------------------------------------------------------- 1 | type PartialNull = { 2 | [P in keyof T]: T[P] | null 3 | } 4 | 5 | interface ModuleOptions { 6 | meta: Record 7 | moduleSideEffects: boolean | 'no-treeshake' 8 | syntheticNamedExports: boolean | string 9 | } 10 | 11 | export interface PartialResolvedId extends Partial> { 12 | external?: boolean | 'absolute' | 'relative' 13 | id: string 14 | } 15 | 16 | export interface SourceDescription extends Partial> { 17 | ast?: unknown /* AcornNode */ 18 | code: string 19 | map?: unknown /* SourceMapInput */ 20 | } 21 | 22 | export type TransformResult = string | null | void | Partial 23 | 24 | export type ResolveIdHook = ( 25 | id: string, 26 | importer?: string | null 27 | ) => Promise 28 | -------------------------------------------------------------------------------- /src/utils/sortObjects.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Meant to be used with `JSON.stringify` to ensure that 3 | * object keys have a consistent order. 4 | */ 5 | export function sortObjects(_key: string, value: any) { 6 | if (value && value.constructor == Object) { 7 | const copy: any = {} 8 | for (const key of Object.keys(value).sort()) { 9 | copy[key] = value[key] 10 | } 11 | return copy 12 | } 13 | return value 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/stripHtmlSuffix.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { stripHtmlSuffix } from './stripHtmlSuffix' 3 | 4 | test('stripHtmlSuffix', () => { 5 | const cases = { 6 | '': '', 7 | '/': '/', 8 | 'index.html': '/', 9 | 'foo.html': '/foo', 10 | '/index.html': '/', 11 | '/foo.html': '/foo', 12 | '/?a=b': '/?a=b', 13 | '/index.html?a=b': '/?a=b', 14 | '/foo.html?a=b': '/foo?a=b', 15 | } 16 | 17 | for (const [input, output] of Object.entries(cases)) { 18 | expect({ input, output: stripHtmlSuffix(input) }).toEqual({ input, output }) 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/utils/stripHtmlSuffix.ts: -------------------------------------------------------------------------------- 1 | const htmlExtensionRE = /\.html(\?|$)/ 2 | const indexHtmlSuffixRE = /\/index.html(\?|$)/ 3 | 4 | export function stripHtmlSuffix(url: string) { 5 | if (!url) { 6 | return url 7 | } 8 | if (url[0] !== '/') { 9 | url = '/' + url 10 | } 11 | if (indexHtmlSuffixRE.test(url)) { 12 | return url.replace(indexHtmlSuffixRE, '/$1') 13 | } 14 | return url.replace(htmlExtensionRE, '$1') 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/take.ts: -------------------------------------------------------------------------------- 1 | export function take(map: Map, key: K): V | undefined { 2 | const value = map.get(key) 3 | map.delete(key) 4 | return value 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/textExtensions.ts: -------------------------------------------------------------------------------- 1 | export const textExtensions = /\.(js|css|svg|json|md|rss|html|xml|txt)$/ 2 | -------------------------------------------------------------------------------- /src/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | export function throttle(run: (cb: () => void) => void) { 2 | let throttled = false 3 | return (cb: () => void) => { 4 | if (!throttled) { 5 | throttled = true 6 | run(() => { 7 | throttled = false 8 | cb() 9 | }) 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "exclude": ["**/*.spec.ts", "dist", "tsup.config.ts"], 5 | "compilerOptions": { 6 | "composite": false, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "incremental": true, 10 | "moduleResolution": "node", 11 | "outDir": "dist", 12 | "tsBuildInfoFile": "dist/.tsbuildinfo" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions' 2 | import { defineConfig } from 'tsup' 3 | 4 | export default defineConfig({ 5 | entry: [ 6 | '**/*.ts', 7 | '!**/*.spec.ts', 8 | '!*.config.ts', 9 | '!node_modules/**', 10 | '!dist/**', 11 | ], 12 | outDir: 'dist', 13 | format: ['cjs', 'esm'], 14 | bundle: false, 15 | plugins: [esbuildPluginFilePathExtensions()], 16 | }) 17 | -------------------------------------------------------------------------------- /src/utils/unwrapDefault.ts: -------------------------------------------------------------------------------- 1 | export function unwrapDefault(module: { default: T }): T 2 | export function unwrapDefault(module: Promise): never 3 | export function unwrapDefault(module: object): T 4 | export function unwrapDefault(module: any): T { 5 | const exported = Object.keys(module) 6 | if (exported.length == 1 && exported[0] == 'default') { 7 | return module.default 8 | } 9 | return module 10 | } 11 | -------------------------------------------------------------------------------- /src/vm/ImporterSet.ts: -------------------------------------------------------------------------------- 1 | import { CompiledModule } from './types' 2 | 3 | export class ImporterSet extends Set { 4 | private _dynamics?: Set 5 | 6 | add(importer: CompiledModule, isDynamic?: boolean) { 7 | if (isDynamic) { 8 | this._dynamics ||= new Set() 9 | this._dynamics.add(importer) 10 | } else { 11 | super.add(importer) 12 | } 13 | return this 14 | } 15 | 16 | delete(importer: CompiledModule) { 17 | const wasStaticImporter = super.delete(importer) 18 | const wasDynamicImporter = !!this._dynamics?.delete(importer) 19 | return wasStaticImporter || wasDynamicImporter 20 | } 21 | 22 | hasDynamic(importer: CompiledModule) { 23 | return !!this._dynamics?.has(importer) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/vm/debug.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | 3 | export const debug = createDebug('saus:vm') 4 | -------------------------------------------------------------------------------- /src/vm/dedupeNodeResolve.ts: -------------------------------------------------------------------------------- 1 | import { bareImportRE } from '@utils/importRegex' 2 | import { join } from 'path' 3 | import { NodeResolveHook } from './hookNodeResolve' 4 | 5 | export function dedupeNodeResolve( 6 | root: string, 7 | dedupe: string[] 8 | ): NodeResolveHook { 9 | const dedupeRE = new RegExp(`^(${dedupe.join('|')})($|/)`) 10 | const dedupeMap: Record = {} 11 | 12 | root = join(root, 'stub.js') 13 | return (id, _importer, nodeResolve) => { 14 | if (bareImportRE.test(id) && dedupeRE.test(id)) { 15 | return (dedupeMap[id] ||= nodeResolve(id, root)) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/vm/dist/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "exports": { 3 | ".": { 4 | "types": "./index.d.ts", 5 | "import": "./index.mjs", 6 | "default": "./index.js" 7 | }, 8 | "./*": { 9 | "types": "./*.d.ts", 10 | "import": "./*.mjs", 11 | "default": "./*.js" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/vm/exportNotFound.ts: -------------------------------------------------------------------------------- 1 | let throwOnMissingExport = 0 2 | 3 | export function setThrowOnMissingExport(enabled: boolean) { 4 | throwOnMissingExport += enabled ? 1 : -1 5 | } 6 | 7 | export const exportNotFound = (file: string) => 8 | new Proxy(Object.prototype, { 9 | get(_, key) { 10 | // Await syntax checks for "then" property to determine 11 | // if this is a promise. 12 | if (throwOnMissingExport > 0 && key !== 'then') { 13 | const err: any = Error( 14 | `The requested module '${file}' does not provide an export named '${ 15 | key as string 16 | }'` 17 | ) 18 | err.framesToPop = 1 19 | throw err 20 | } 21 | }, 22 | }) 23 | -------------------------------------------------------------------------------- /src/vm/forceNodeReload.ts: -------------------------------------------------------------------------------- 1 | import createDebug from 'debug' 2 | import { Module } from 'module' 3 | import { NodeModule } from './nodeModules' 4 | 5 | const debug = createDebug('saus:forceNodeReload') 6 | 7 | type ShouldReloadFn = (id: string, module: NodeModule) => boolean 8 | 9 | export function forceNodeReload(shouldReload: ShouldReloadFn) { 10 | const rawCache = (Module as any)._cache as Record 11 | 12 | // @ts-ignore 13 | Module._cache = new Proxy(rawCache, { 14 | get(_, id: string) { 15 | const cached = rawCache[id] 16 | if (!cached || !shouldReload(id, cached)) { 17 | return cached 18 | } 19 | debug('Forcing reload: %s', id) 20 | delete rawCache[id] 21 | }, 22 | }) 23 | 24 | return () => { 25 | // @ts-ignore 26 | Module._cache = rawCache 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/vm/fullReload.ts: -------------------------------------------------------------------------------- 1 | import { Module } from 'module' 2 | 3 | /** 4 | * Create a `shouldReload` function that reloads almost every 5 | * SSR module, avoiding multiple instances of any one module. 6 | */ 7 | export function createFullReload(reloadList = new Set()) { 8 | const loadedIds = Object.keys((Module as any)._cache) 9 | const skippedInternals = /\/saus\/(?!client|examples|packages)/ 10 | 11 | return (id: string) => { 12 | // Module was possibly cached during the full reload. 13 | if (!loadedIds.includes(id)) { 14 | return false 15 | } 16 | // Modules are reloaded just once per full reload. 17 | if (reloadList.has(id)) { 18 | return false 19 | } 20 | // Internal modules should never be reloaded. 21 | if (skippedInternals.test(id)) { 22 | return false 23 | } 24 | reloadList.add(id) 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/vm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './asyncRequire' 2 | export * from './compileEsm' 3 | export * from './debug' 4 | export * from './dedupeNodeResolve' 5 | export * from './executeModule' 6 | export * from './forceNodeReload' 7 | export * from './formatAsyncStack' 8 | export * from './hookNodeResolve' 9 | export * from './ImporterSet' 10 | export * from './moduleMap' 11 | export * from './nodeModules' 12 | export * from './traceNodeRequire' 13 | export * from './types' 14 | -------------------------------------------------------------------------------- /src/vm/isLiveModule.ts: -------------------------------------------------------------------------------- 1 | import { Merge } from 'type-fest' 2 | import { CompiledModule, isLinkedModule, LinkedModule } from './types' 3 | 4 | /** 5 | * Live modules must have a `module.exports` value that's a plain object 6 | * and its exports must not be destructured by importers. 7 | */ 8 | export function isLiveModule( 9 | module: CompiledModule | LinkedModule, 10 | liveModulePaths: Set 11 | ): module is Merge }> { 12 | return ( 13 | isLinkedModule(module) && 14 | module.exports?.constructor == Object && 15 | liveModulePaths.has(module.id) 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/vm/overwriteScript.ts: -------------------------------------------------------------------------------- 1 | import { combineSourcemaps } from '@utils/combineSourcemaps' 2 | import type { SourceMap } from '@utils/node/sourceMap' 3 | import { Script } from './types' 4 | 5 | export function overwriteScript( 6 | filename: string, 7 | oldScript: Script, 8 | newScript: { code: string; map?: any } 9 | ): Script { 10 | let map: SourceMap | undefined 11 | if (oldScript.map && newScript.map) { 12 | map = combineSourcemaps(filename, [ 13 | newScript.map, 14 | oldScript.map as any, 15 | ]) as any 16 | } else { 17 | map = newScript.map || oldScript.map 18 | } 19 | return { 20 | code: newScript.code, 21 | map, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/vm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saus/vm", 3 | "version": "0.4.10", 4 | "scripts": { 5 | "clean": "rimraf dist && git checkout HEAD dist", 6 | "build": "npm run clean && tsup-node && tsc -p . --emitDeclarationOnly", 7 | "dev": "concurrently npm:dev:*", 8 | "dev:build": "tsup-node --watch --sourcemap", 9 | "dev:types": "tsc -p . --emitDeclarationOnly --watch" 10 | }, 11 | "dependencies": { 12 | "@saus/utils": "workspace:*", 13 | "builtin-modules": "^3.2.0", 14 | "debug": "^4.3.2", 15 | "es-module-lexer": "0.9.3", 16 | "kleur": "^4.1.4", 17 | "type-fest": "^2.13.0" 18 | }, 19 | "devDependencies": { 20 | "@types/babel__traverse": "^7.18.2", 21 | "@utils": "link:./node_modules/@saus/utils/dist" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/vm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "exclude": ["**/*.spec.ts", "dist", "tsup.config.ts"], 5 | "compilerOptions": { 6 | "composite": false, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "incremental": true, 10 | "moduleResolution": "node", 11 | "outDir": "dist", 12 | "tsBuildInfoFile": "dist/.tsbuildinfo" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/vm/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions' 2 | import { defineConfig } from 'tsup' 3 | 4 | export default defineConfig({ 5 | entry: [ 6 | '**/*.ts', 7 | '!**/*.spec.ts', 8 | '!*.config.ts', 9 | '!node_modules/**', 10 | '!dist/**', 11 | ], 12 | outDir: 'dist', 13 | format: ['cjs', 'esm'], 14 | bundle: false, 15 | plugins: [esbuildPluginFilePathExtensions()], 16 | }) 17 | -------------------------------------------------------------------------------- /test/config.ts: -------------------------------------------------------------------------------- 1 | import { vite } from '@/vite' 2 | import { resolve } from 'path' 3 | import { vi } from 'vitest' 4 | 5 | let configFile: { 6 | path: string 7 | config: vite.UserConfig 8 | dependencies: string[] 9 | } 10 | 11 | export const setConfigFile = (root: string, config: vite.UserConfig) => 12 | (configFile = { 13 | path: resolve(root, 'vite.config.js'), 14 | dependencies: [], 15 | config: { 16 | ...config, 17 | root, 18 | }, 19 | }) 20 | 21 | vi.mock('@/vite/configFile', (): typeof import('@/vite/configFile') => { 22 | return { 23 | loadConfigFile: async () => configFile, 24 | } 25 | }) 26 | 27 | vi.mock('@/vite/configDeps', (): typeof import('@/vite/configDeps') => { 28 | return { 29 | loadConfigDeps: async (_command, { plugins }) => ({ 30 | plugins, 31 | }), 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config' 2 | export * from './context' 3 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['src/**/*.spec.{ts,tsx}'], 6 | }, 7 | server: { 8 | watch: { 9 | ignored: ['**/vendor/**'], 10 | }, 11 | }, 12 | }) 13 | --------------------------------------------------------------------------------