├── .env.example
├── .gitignore
├── .hooks
└── pre-commit
├── .vscode
├── extensions.json
└── settings.example.json
├── Dockerfile.site
├── LICENSE
├── Makefile
├── README.md
├── RELEASE_INSTRUCTIONS.md
├── biome.jsonc
├── bootstrap
├── bootstrap.go
├── tmpls
│ ├── app_go_tmpl.txt
│ ├── backend_router_actions_go_tmpl.txt
│ ├── backend_router_core_go_tmpl.txt
│ ├── backend_router_loaders_go_tmpl.txt
│ ├── backend_router_tasks_registry_go_tmpl.txt
│ ├── backend_server_server_go_tmpl.txt
│ ├── backend_static_entry_go_html_str.txt
│ ├── cmd_app_main_go_tmpl.txt
│ ├── cmd_build_main_go_tmpl.txt
│ ├── dist_static_keep_tmpl.txt
│ ├── frontend_app_tsx_tmpl.txt
│ ├── frontend_app_utils_ts_tmpl.txt
│ ├── frontend_entry_tsx_preact_str.txt
│ ├── frontend_entry_tsx_react_str.txt
│ ├── frontend_entry_tsx_solid_str.txt
│ ├── frontend_home_tsx_tmpl.txt
│ ├── frontend_routes_ts_str.txt
│ ├── gitignore_str.txt
│ ├── main_critical_css_str.txt
│ ├── main_css_str.txt
│ ├── package_json_str.txt
│ ├── ts_config_json_tmpl.txt
│ ├── vite_config_ts_tmpl.txt
│ └── wave_config_json_tmpl.txt
└── utils.go
├── go.mod
├── go.sum
├── internal
├── framework
│ ├── _typescript
│ │ ├── client
│ │ │ ├── index.ts
│ │ │ ├── src
│ │ │ │ ├── client.ts
│ │ │ │ ├── head.test.ts
│ │ │ │ ├── head.ts
│ │ │ │ ├── history_types.ts
│ │ │ │ ├── impl_helpers.ts
│ │ │ │ ├── redirects.ts
│ │ │ │ ├── river_ctx.ts
│ │ │ │ ├── route_def_helpers.ts
│ │ │ │ └── utils.ts
│ │ │ ├── tests
│ │ │ │ └── client.test.ts
│ │ │ └── tsconfig.json
│ │ ├── preact
│ │ │ ├── index.tsx
│ │ │ ├── src
│ │ │ │ ├── helpers.ts
│ │ │ │ ├── link.tsx
│ │ │ │ └── preact.tsx
│ │ │ └── tsconfig.json
│ │ ├── react
│ │ │ ├── index.tsx
│ │ │ ├── src
│ │ │ │ ├── helpers.ts
│ │ │ │ ├── link.tsx
│ │ │ │ └── react.tsx
│ │ │ └── tsconfig.json
│ │ └── solid
│ │ │ ├── index.tsx
│ │ │ ├── src
│ │ │ ├── helpers.ts
│ │ │ ├── link.tsx
│ │ │ └── solid.tsx
│ │ │ └── tsconfig.json
│ ├── get_deps.go
│ ├── get_root_handler.go
│ ├── gmpd.go
│ ├── river_build.go
│ ├── river_core.go
│ ├── river_gen_ts.go
│ ├── river_init.go
│ ├── ssr.go
│ └── vite_cmd.go
├── junk-drawer
│ └── playground
│ │ ├── embedded
│ │ └── main.go
│ │ ├── matcher
│ │ └── main.go
│ │ ├── mux
│ │ └── main.go
│ │ ├── nested
│ │ └── main.go
│ │ ├── singletypegen
│ │ └── main.go
│ │ ├── tasks
│ │ └── main.go
│ │ ├── tstyper
│ │ └── main.go
│ │ └── validate
│ │ └── main.go
└── scripts
│ ├── _typescript
│ └── tsconfig.json
│ ├── buildts
│ ├── build-solid.mjs
│ └── main.go
│ ├── bumper
│ └── main.go
│ └── npm_bumper
│ └── main.go
├── kit
├── _typescript
│ ├── converters
│ │ ├── converters.test.ts
│ │ └── converters.ts
│ ├── cookies
│ │ └── cookies.ts
│ ├── debounce
│ │ ├── debounce.test.ts
│ │ └── debounce.ts
│ ├── fmt
│ │ └── fmt.ts
│ ├── json
│ │ ├── deep_equals.test.ts
│ │ ├── deep_equals.ts
│ │ ├── json.ts
│ │ ├── search_param_serializer.ts
│ │ ├── search_params_serializer.test.ts
│ │ ├── stringify_stable.test.ts
│ │ └── stringify_stable.ts
│ ├── listeners
│ │ └── listeners.ts
│ ├── theme
│ │ └── theme.ts
│ ├── tsconfig.json
│ └── url
│ │ ├── url.test.ts
│ │ └── url.ts
├── bytesutil
│ ├── bytesutil.go
│ └── bytesutil_test.go
├── cliutil
│ └── cliutil.go
├── colorlog
│ ├── colorlog.go
│ └── colorlog_test.go
├── contextutil
│ ├── contextutil.go
│ └── contextutil_test.go
├── cryptoutil
│ ├── cryptoutil.go
│ └── cryptoutil_test.go
├── dedupe
│ └── dedupe.go
├── dirs
│ ├── dirs.go
│ └── dirs_test.go
├── envutil
│ ├── envutil.go
│ └── envutil_test.go
├── errutil
│ └── errutil.go
├── esbuildutil
│ └── esbuildutil.go
├── executil
│ ├── executil.go
│ └── executil_test.go
├── fsutil
│ ├── fsutil.go
│ └── fsutil_test.go
├── genericsutil
│ ├── genericsutil.go
│ └── genericsutil_test.go
├── grace
│ ├── grace.go
│ └── grace_test.go
├── headels
│ ├── headblocks.go
│ ├── headblocks_new_test.go
│ └── headblocks_test.go
├── htmltestutil
│ └── htmltestutil.go
├── htmlutil
│ ├── htmlutil.go
│ └── htmlutil_test.go
├── id
│ ├── id.go
│ └── id_test.go
├── ioutil
│ ├── ioutil.go
│ └── ioutil_test.go
├── jsonschema
│ └── jsonschema.go
├── jsonutil
│ ├── jsonutil.go
│ └── jsonutil_test.go
├── lazyget
│ ├── lazyget.go
│ └── lazyget_test.go
├── lru
│ ├── lru.go
│ └── lru_test.go
├── matcher
│ ├── bench.txt
│ ├── find_best_match.go
│ ├── find_best_match_test.go
│ ├── find_nested_matches.go
│ ├── find_nested_matches_test.go
│ ├── matcher.go
│ ├── parse_segments.go
│ ├── parse_segments_test.go
│ └── register.go
├── middleware
│ ├── csrftoken
│ │ ├── csrftoken.go
│ │ └── csrftoken_test.go
│ ├── etag
│ │ ├── etag.go
│ │ └── etag_test.go
│ ├── healthcheck
│ │ └── healthcheck.go
│ ├── middleware.go
│ ├── robotstxt
│ │ └── robotstxt.go
│ └── secureheaders
│ │ ├── secureheaders.go
│ │ └── secureheaders_test.go
├── mux
│ ├── bench.txt
│ ├── mux.go
│ ├── nested_mux.go
│ └── router_test.go
├── opt
│ └── opt.go
├── parseutil
│ └── parseutil.go
├── port
│ └── port.go
├── reflectutil
│ └── reflectutil.go
├── response
│ ├── proxy.go
│ ├── response.go
│ └── response_test.go
├── rpc
│ ├── rpc.go
│ └── rpc_test.go
├── safecache
│ ├── safecache.go
│ └── safecache_test.go
├── scripts
│ └── bumper
│ │ └── bumper.go
├── securestring
│ ├── securestring.go
│ └── securestring_test.go
├── set
│ └── set.go
├── signedcookie
│ ├── signedcookie.go
│ └── signedcookie_test.go
├── sqlutil
│ └── sqlutil.go
├── stringsutil
│ ├── collect_lines.go
│ └── stringsutil.go
├── tasks
│ ├── tasks.go
│ └── tasks_test.go
├── theme
│ └── theme.go
├── timer
│ └── timer.go
├── tsgen
│ ├── generate_ts_content.go
│ ├── generate_ts_content_test.go
│ ├── statements.go
│ ├── to_file.go
│ └── tsgencore
│ │ ├── collector.go
│ │ └── core.go
├── typed
│ ├── syncmap.go
│ └── syncmap_test.go
├── validate
│ ├── error_acc_test.go
│ ├── error_collector.go
│ ├── error_collector_test.go
│ ├── more_error_collector_test.go
│ ├── rules.go
│ ├── rules_test.go
│ ├── search_params.go
│ ├── search_params_test.go
│ ├── validate.go
│ └── validate_test.go
├── viteutil
│ ├── cmd.go
│ └── viteutil.go
├── walkutil
│ └── walkutil.go
└── xyz
│ ├── fsmarkdown
│ └── fsmarkdown.go
│ └── xyz.go
├── package.json
├── pnpm-lock.yaml
├── river.go
├── site
├── .gitignore
├── Makefile
├── __cmd
│ ├── app
│ │ └── main.go
│ └── build
│ │ └── main.go
├── __dist
│ └── static
│ │ └── .keep
├── app.go
├── backend
│ ├── __static
│ │ ├── entry.go.html
│ │ └── markdown
│ │ │ ├── faq.md
│ │ │ └── start.md
│ ├── markdown
│ │ └── markdown.go
│ ├── router
│ │ ├── actions.go
│ │ ├── core.go
│ │ └── loaders.go
│ └── server
│ │ └── server.go
├── frontend
│ ├── __static
│ │ ├── desktop.svg
│ │ ├── favicon.svg
│ │ ├── full-logo.svg
│ │ ├── logo.svg
│ │ ├── moon.svg
│ │ ├── river-banner.webp
│ │ └── sun.svg
│ ├── components
│ │ ├── app.tsx
│ │ ├── app_link.tsx
│ │ ├── app_utils.ts
│ │ ├── global_loader.ts
│ │ ├── highlight.ts
│ │ ├── rendered-markdown.tsx
│ │ └── routes
│ │ │ ├── dyn.tsx
│ │ │ ├── home.tsx
│ │ │ └── md.tsx
│ ├── css
│ │ ├── hljs.css
│ │ ├── main.critical.css
│ │ ├── main.css
│ │ ├── nprogress.css
│ │ └── tailwind-input.css
│ ├── entry.tsx
│ ├── river.gen.ts
│ └── routes.ts
├── go.mod
├── go.sum
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
├── vite.config.ts
└── wave.config.json
├── tsconfig.base.json
├── vitest.config.ts
└── wave
├── internal
└── ki
│ ├── build.go
│ ├── buildhelper.go
│ ├── config.go
│ ├── configschema
│ └── main.go
│ ├── css.go
│ ├── css_test.go
│ ├── debouncer.go
│ ├── dev_core.go
│ ├── dev_events.go
│ ├── dev_vite.go
│ ├── dist.go
│ ├── dumb_little_helpers.go
│ ├── env.go
│ ├── env_test.go
│ ├── evt_utils.go
│ ├── file_hash.go
│ ├── filemap.go
│ ├── ik_test.go
│ ├── main_init.go
│ ├── middleware.go
│ ├── on_change.go
│ ├── pattern_utils.go
│ ├── port.go
│ ├── public_asset_keys.go
│ ├── readiness.go
│ ├── refresh.go
│ ├── setup.go
│ ├── static_assets.go
│ ├── universal_fs.go
│ └── universal_fs_test.go
└── wave.go
/.env.example:
--------------------------------------------------------------------------------
1 | GITHUB_TOKEN = ghp_123
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | .DS_Store
4 |
5 | *.local
6 | *.local.*
7 |
8 | .env
9 | .env.*
10 | !.env.example
11 |
12 | .vscode/settings.json
13 |
14 | npm_dist/
15 |
--------------------------------------------------------------------------------
/.hooks/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #####################################################################
4 | #
5 | # You must initialize this pre-commit hook by running the following
6 | # commands:
7 | #
8 | # ```sh
9 | # git config core.hooksPath .hooks
10 | # chmod +x .hooks/pre-commit
11 | # ```
12 | #
13 | # After running the above, this pre-commit hook will run before
14 | # every commit, and if it returns a non-zero exit code, the commit
15 | # will be aborted. You can test the script's behavior by executing
16 | # it directly: `.hooks/pre-commit`.
17 | #
18 | #####################################################################
19 |
20 | set -e
21 |
22 | run_test() {
23 | echo "[pre-commit] INFO Running cmd '$1'."
24 | $1
25 | if [ $? -ne 0 ]; then
26 | echo "[pre-commit] ERROR Cmd '$1' failed. Commit aborted."
27 | exit 1
28 | fi
29 | echo "[pre-commit] OK Cmd '$1' passed."
30 | }
31 |
32 | run_test "make gotest"
33 | run_test "make tstest"
34 | run_test "make tslint"
35 | run_test "make tscheck"
36 |
37 | echo "[pre-commit] OK Done."
38 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["biomejs.biome", "golang.go", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": {
3 | "source.organizeImports.biome": "explicit",
4 | "source.fixAll.biome": "explicit"
5 | },
6 |
7 | "search.exclude": {
8 | "**/node_modules": true,
9 | "**/river.gen.ts": true
10 | },
11 |
12 | "javascript.preferences.importModuleSpecifierEnding": "js",
13 | "typescript.preferences.importModuleSpecifierEnding": "js",
14 |
15 | "[go]": {
16 | "editor.defaultFormatter": "golang.go"
17 | },
18 |
19 | "[html][css][json][jsonc][javascript][typescript][javascriptreact][typescriptreact]": {
20 | "editor.defaultFormatter": "biomejs.biome"
21 | },
22 |
23 | "[markdown]": {
24 | "editor.defaultFormatter": "esbenp.prettier-vscode",
25 | "prettier.proseWrap": "always"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Dockerfile.site:
--------------------------------------------------------------------------------
1 | ### builder-base
2 | FROM golang:1.24 as builder-base
3 | WORKDIR /monorepo
4 | RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
5 | RUN apt-get update
6 | RUN apt-get install -y --no-install-recommends nodejs make
7 | RUN npm i -g pnpm
8 | RUN apt-get clean
9 | RUN rm -rf /var/lib/apt/lists/*
10 |
11 | ### river-npm-deps
12 | FROM builder-base as river-npm-deps
13 | WORKDIR /monorepo
14 | COPY package.json pnpm-lock.yaml ./
15 | RUN pnpm i --frozen-lockfile
16 | COPY . .
17 | RUN make npmbuild
18 |
19 | ### frontend-deps
20 | FROM node:22-alpine as frontend-deps
21 | WORKDIR /monorepo
22 | RUN npm i -g pnpm
23 | COPY --from=river-npm-deps /monorepo/npm_dist ./npm_dist
24 | COPY site/package.json site/pnpm-lock.yaml ./site/
25 | WORKDIR /monorepo/site
26 | RUN pnpm i --frozen-lockfile
27 | COPY site/ ./
28 |
29 | ### backend-builder
30 | FROM builder-base as backend-builder
31 | WORKDIR /monorepo
32 | COPY . .
33 | COPY --from=river-npm-deps /monorepo/npm_dist ./npm_dist
34 | COPY --from=frontend-deps /monorepo/site ./site
35 | WORKDIR /monorepo/site
36 | RUN go mod download
37 | RUN npx @tailwindcss/cli \
38 | -i ./frontend/css/tailwind-input.css \
39 | -o ./frontend/css/tailwind-output.css
40 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
41 | -o /tmp/build_tool \
42 | ./__cmd/build
43 | RUN /tmp/build_tool --no-binary
44 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -v \
45 | -o /monorepo/__main \
46 | ./__cmd/app
47 |
48 | ### server
49 | FROM alpine:latest
50 | WORKDIR /app
51 | RUN apk --no-cache add ca-certificates
52 | COPY --from=backend-builder /monorepo/__main .
53 | RUN adduser -D serveruser
54 | USER serveruser
55 | ENTRYPOINT ["/app/__main"]
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright 2023 Samuel J. Cook
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | #####################################################################
2 | ####### GO
3 | #####################################################################
4 |
5 | gotest:
6 | @go test -race ./...
7 |
8 | gotestloud:
9 | @go test -race -v ./...
10 |
11 | gobump: gotest
12 | @go run ./internal/scripts/bumper
13 |
14 | # call with `make gobench pkg=./kit/mux` (or whatever)
15 | gobench:
16 | @go test -bench=. $(pkg)
17 |
18 | #####################################################################
19 | ####### TS
20 | #####################################################################
21 |
22 | tstest:
23 | @pnpm vitest run
24 |
25 | tstestwatch:
26 | @pnpm vitest
27 |
28 | tsreset:
29 | @rm -rf node_modules 2>/dev/null || true
30 | @find . -path "*/node_modules" -type d -exec rm -rf {} \; 2>/dev/null || true
31 | @pnpm i
32 |
33 | tslint:
34 | @pnpm biome check .
35 |
36 | tscheck: tscheck-kit tscheck-fw-client tscheck-fw-react tscheck-fw-solid
37 |
38 | tscheck-kit:
39 | @pnpm tsc --noEmit --project ./kit/_typescript
40 |
41 | tscheck-fw-client:
42 | @pnpm tsc --noEmit --project ./internal/framework/_typescript/client
43 |
44 | tscheck-fw-react:
45 | @pnpm tsc --noEmit --project ./internal/framework/_typescript/react
46 |
47 | tscheck-fw-solid:
48 | @pnpm tsc --noEmit --project ./internal/framework/_typescript/solid
49 |
50 | tscheck-fw-preact:
51 | @pnpm tsc --noEmit --project ./internal/framework/_typescript/preact
52 |
53 | tsprepforpub: tsreset tstest tslint tscheck
54 |
55 | tspublishpre: tsprepforpub
56 | @npm publish --access public --tag pre
57 |
58 | tspublishnonpre: tsprepforpub
59 | @npm publish --access public
60 |
61 | npmbuild:
62 | @go run ./internal/scripts/buildts
63 |
64 | npmbump:
65 | @go run ./internal/scripts/npm_bumper
66 |
67 | docker-site:
68 | @docker build -t river-site -f Dockerfile.site .
69 |
70 | docker-run-site:
71 | docker run -d -p $(PORT):$(PORT) -e PORT=$(PORT) river-site
72 |
--------------------------------------------------------------------------------
/RELEASE_INSTRUCTIONS.md:
--------------------------------------------------------------------------------
1 | 1. bump package.json / run build / publish to npm
2 |
3 | ```sh
4 | make npmbump
5 | ```
6 |
7 | 2. push to github
8 |
9 | ```sh
10 | git add .
11 | git commit -m 'v0.0.0-pre.0'
12 | git push
13 | ```
14 |
15 | 3. publish to go proxy / push version tag
16 |
17 | ```sh
18 | make gobump
19 | ```
20 |
21 | 4. profit
22 |
--------------------------------------------------------------------------------
/biome.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/2.0.0-beta.5/schema.json",
3 | "vcs": {
4 | "enabled": false,
5 | "clientKind": "git",
6 | "useIgnoreFile": false
7 | },
8 | "files": {
9 | "ignoreUnknown": false,
10 | "includes": [
11 | "**",
12 | "!**/node_modules",
13 | "!npm_dist/**/*",
14 | "!**/__dist",
15 | "!**/frontend/river.gen.ts",
16 | "!**/frontend/css/tailwind-output.css"
17 | ]
18 | },
19 | "formatter": {
20 | "enabled": true,
21 | "indentStyle": "tab",
22 | "lineWidth": 87,
23 | "includes": ["**"]
24 | },
25 | "assist": {
26 | "actions": {
27 | "source": {
28 | "organizeImports": "on"
29 | }
30 | }
31 | },
32 | "linter": {
33 | "enabled": true,
34 | "includes": ["**"],
35 | "rules": {
36 | "nursery": {
37 | "noImportCycles": "error"
38 | },
39 | "style": {
40 | "useTemplate": "off", // permit string concatenation
41 | "useLiteralEnumMembers": "error",
42 | "noCommaOperator": "error",
43 | "useNodejsImportProtocol": "error",
44 | "useAsConstAssertion": "error",
45 | "useEnumInitializers": "error",
46 | "useSelfClosingElements": "error",
47 | "useConst": "error",
48 | "useSingleVarDeclarator": "error",
49 | "noUnusedTemplateLiteral": "error",
50 | "useNumberNamespace": "error",
51 | "noInferrableTypes": "error",
52 | "useExponentiationOperator": "error",
53 | "noParameterAssign": "error",
54 | "noNonNullAssertion": "error",
55 | "useDefaultParameterLast": "error",
56 | "noArguments": "error",
57 | "useImportType": "error",
58 | "useExportType": "error",
59 | "noUselessElse": "error",
60 | "useShorthandFunctionType": "error",
61 | "useConsistentArrayType": {
62 | "level": "error",
63 | "options": {
64 | "syntax": "generic"
65 | }
66 | },
67 | "useBlockStatements": "error"
68 | },
69 | "correctness": {
70 | "useImportExtensions": "error" // require ".ts", etc. in import statements
71 | },
72 | "suspicious": {
73 | "noExplicitAny": "off" // permit explicit any
74 | },
75 | "a11y": {
76 | "noStaticElementInteractions": "off"
77 | }
78 | }
79 | },
80 | "javascript": {
81 | "formatter": {
82 | "quoteStyle": "double"
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/app_go_tmpl.txt:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "embed"
5 | "net/http"
6 |
7 | "github.com/river-now/river"
8 | "github.com/river-now/river/kit/colorlog"
9 | "github.com/river-now/river/kit/headels"
10 | "github.com/river-now/river/kit/htmlutil"
11 | "github.com/river-now/river/wave"
12 | )
13 |
14 | var River = &river.River{
15 | Wave: Wave,
16 | GetHeadElUniqueRules: func() *headels.HeadEls {
17 | e := river.NewHeadEls(2)
18 |
19 | e.Meta(e.Property("og:title"))
20 | e.Meta(e.Property("og:description"))
21 |
22 | return e
23 | },
24 | GetDefaultHeadEls: func(r *http.Request) ([]*htmlutil.Element, error) {
25 | e := river.NewHeadEls()
26 |
27 | e.Title("River Example")
28 | e.Description("This is a River example.")
29 |
30 | return e.Collect(), nil
31 | },
32 | GetRootTemplateData: func(r *http.Request) (map[string]any, error) {
33 | // This gets fed into backend/__static/entry.go.html
34 | return map[string]any{}, nil
35 | },
36 | }
37 |
38 | //go:embed wave.config.json
39 | var configBytes []byte
40 |
41 | //go:embed all:__dist/static
42 | var staticFS embed.FS
43 |
44 | var Wave = wave.New(&wave.Config{
45 | ConfigBytes: configBytes,
46 | StaticFS: staticFS,
47 | StaticFSEmbedDirective: "all:__dist/static",
48 | })
49 |
50 | var Log = colorlog.New("{{.GoImportBase}}")
51 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/backend_router_actions_go_tmpl.txt:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 |
7 | "github.com/river-now/river/kit/mux"
8 | "github.com/river-now/river/kit/validate"
9 | )
10 |
11 | var ActionsRouter = mux.NewRouter(&mux.Options{
12 | TasksRegistry: SharedTasksRegistry,
13 | MountRoot: "/api/",
14 | MarshalInput: func(r *http.Request, iPtr any) error {
15 | if r.Method == http.MethodGet {
16 | return validate.URLSearchParamsInto(r, iPtr)
17 | }
18 | if r.Method == http.MethodPost {
19 | return validate.JSONBodyInto(r, iPtr)
20 | }
21 | return errors.New("unsupported method")
22 | },
23 | })
24 |
25 | type ActionCtx[I any] struct {
26 | *mux.ReqData[I]
27 | // Anything else you want available on the ActionCtx
28 | }
29 |
30 | func NewAction[I any, O any](method, pattern string, f func(c *ActionCtx[I]) (O, error)) *mux.TaskHandler[I, O] {
31 | wrappedF := func(c *mux.ReqData[I]) (O, error) {
32 | return f(&ActionCtx[I]{
33 | ReqData: c,
34 | // Anything else you want available on the ActionCtx
35 | })
36 | }
37 | actionTask := mux.TaskHandlerFromFunc(ActionsRouter.TasksRegistry(), wrappedF)
38 | mux.RegisterTaskHandler(ActionsRouter, method, pattern, actionTask)
39 | return actionTask
40 | }
41 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/backend_router_core_go_tmpl.txt:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | app "{{.GoImportBase}}"
5 | "net/http"
6 | "strings"
7 |
8 | chimw "github.com/go-chi/chi/v5/middleware"
9 | "github.com/river-now/river/kit/middleware/etag"
10 | "github.com/river-now/river/kit/middleware/healthcheck"
11 | "github.com/river-now/river/kit/middleware/robotstxt"
12 | "github.com/river-now/river/kit/middleware/secureheaders"
13 | "github.com/river-now/river/kit/mux"
14 | )
15 |
16 | func Core() *mux.Router {
17 | r := mux.NewRouter(nil)
18 |
19 | mux.SetGlobalHTTPMiddleware(r, chimw.Logger)
20 | mux.SetGlobalHTTPMiddleware(r, chimw.Recoverer)
21 | mux.SetGlobalHTTPMiddleware(r, etag.Auto(&etag.Config{
22 | SkipFunc: func(r *http.Request) bool {
23 | return strings.HasPrefix(r.URL.Path, app.Wave.GetPublicPathPrefix())
24 | },
25 | }))
26 | mux.SetGlobalHTTPMiddleware(r, secureheaders.Middleware)
27 | mux.SetGlobalHTTPMiddleware(r, healthcheck.Healthz)
28 | mux.SetGlobalHTTPMiddleware(r, robotstxt.Allow)
29 | mux.SetGlobalHTTPMiddleware(r, app.Wave.FaviconRedirect())
30 |
31 | // static public assets
32 | mux.RegisterHandler(r, "GET", app.Wave.GetPublicPathPrefix()+"*", app.Wave.MustGetServeStaticHandler(true))
33 |
34 | // river UI routes
35 | mux.RegisterHandler(r, "GET", "/*", app.River.GetUIHandler(LoadersRouter))
36 |
37 | // river API routes
38 | actionsHandler := app.River.GetActionsHandler(ActionsRouter)
39 | mux.RegisterHandler(r, "GET", ActionsRouter.MountRoot("*"), actionsHandler)
40 | mux.RegisterHandler(r, "POST", ActionsRouter.MountRoot("*"), actionsHandler)
41 |
42 | return r
43 | }
44 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/backend_router_loaders_go_tmpl.txt:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import "github.com/river-now/river/kit/mux"
4 |
5 | var LoadersRouter = mux.NewNestedRouter(&mux.NestedOptions{
6 | TasksRegistry: SharedTasksRegistry,
7 | ExplicitIndexSegment: "_index", // Optional, but recommended
8 | })
9 |
10 | type LoaderCtx struct {
11 | *mux.NestedReqData
12 | // Anything else you want available on the LoaderCtx
13 | }
14 |
15 | func NewLoader[O any](pattern string, f func(c *LoaderCtx) (O, error)) *mux.TaskHandler[mux.None, O] {
16 | wrappedF := func(c *mux.NestedReqData) (O, error) {
17 | return f(&LoaderCtx{
18 | NestedReqData: c,
19 | // Anything else you want available on the LoaderCtx
20 | })
21 | }
22 | loaderTask := mux.TaskHandlerFromFunc(LoadersRouter.TasksRegistry(), wrappedF)
23 | mux.RegisterNestedTaskHandler(LoadersRouter, pattern, loaderTask)
24 | return loaderTask
25 | }
26 |
27 | type RootData struct {
28 | Message string
29 | }
30 |
31 | // Example loader
32 | var _ = NewLoader("/", func(c *LoaderCtx) (*RootData, error) {
33 | return &RootData{Message: "Hello from a River loader!"}, nil
34 | })
35 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/backend_router_tasks_registry_go_tmpl.txt:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import "github.com/river-now/river/kit/tasks"
4 |
5 | var SharedTasksRegistry = tasks.NewRegistry("{{.GoImportBase}}")
6 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/backend_server_server_go_tmpl.txt:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | app "{{.GoImportBase}}"
5 | "{{.GoImportBase}}/backend/router"
6 | "context"
7 | "fmt"
8 | "log"
9 | "net/http"
10 | "os"
11 | "time"
12 |
13 | "github.com/river-now/river/kit/grace"
14 | "github.com/river-now/river/wave"
15 | )
16 |
17 | func Serve() {
18 | app.River.Init(wave.GetIsDev())
19 |
20 | addr := fmt.Sprintf(":%d", wave.MustGetPort())
21 |
22 | server := &http.Server{
23 | Addr: addr,
24 | Handler: http.TimeoutHandler(router.Core(), 60*time.Second, "Request timed out"),
25 | ReadTimeout: 15 * time.Second,
26 | WriteTimeout: 30 * time.Second,
27 | IdleTimeout: 60 * time.Second,
28 | ReadHeaderTimeout: 10 * time.Second,
29 | MaxHeaderBytes: 1 << 20, // 1 MB
30 | DisableGeneralOptionsHandler: true,
31 | ErrorLog: log.New(os.Stderr, "HTTP: ", log.Ldate|log.Ltime|log.Lshortfile),
32 | }
33 |
34 | url := "http://localhost" + addr
35 |
36 | grace.Orchestrate(grace.OrchestrateOptions{
37 | StartupCallback: func() error {
38 | app.Log.Info("Starting server", "url", url)
39 |
40 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
41 | log.Fatalf("Server listen and serve error: %v\n", err)
42 | }
43 |
44 | return nil
45 | },
46 |
47 | ShutdownCallback: func(shutdownCtx context.Context) error {
48 | app.Log.Info("Shutting down server", "url", url)
49 |
50 | if err := server.Shutdown(shutdownCtx); err != nil {
51 | log.Fatalf("Server shutdown error: %v\n", err)
52 | }
53 |
54 | return nil
55 | },
56 | })
57 | }
58 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/backend_static_entry_go_html_str.txt:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{.RiverHeadEls}}
7 | {{.RiverSSRScript}}
8 |
9 |
10 |
11 | {{.RiverBodyScripts}}
12 |
13 |
14 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/cmd_app_main_go_tmpl.txt:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "{{.GoImportBase}}/backend/server"
4 |
5 | func main() {
6 | server.Serve()
7 | }
8 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/cmd_build_main_go_tmpl.txt:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | app "{{.GoImportBase}}"
5 | "{{.GoImportBase}}/backend/router"
6 |
7 | "github.com/river-now/river"
8 | "github.com/river-now/river/kit/tsgen"
9 | )
10 |
11 | func main() {
12 | a := tsgen.Statements{}
13 |
14 | a.Serialize("export const ACTIONS_ROUTER_MOUNT_ROOT", router.ActionsRouter.MountRoot())
15 |
16 | app.Wave.Builder(func(isDev bool) error {
17 | return app.River.Build(&river.BuildOptions{
18 | IsDev: isDev,
19 | LoadersRouter: router.LoadersRouter,
20 | ActionsRouter: router.ActionsRouter,
21 | AdHocTypes: []*river.AdHocType{},
22 | ExtraTSCode: a.BuildString(),
23 | })
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/dist_static_keep_tmpl.txt:
--------------------------------------------------------------------------------
1 | //go:embed directives require at least one file to compile
2 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/frontend_app_tsx_tmpl.txt:
--------------------------------------------------------------------------------
1 | import { RiverRootOutlet } from "river.now/{{.UIVariant}}";
2 |
3 | export function App() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/frontend_app_utils_ts_tmpl.txt:
--------------------------------------------------------------------------------
1 | import {
2 | makeTypedAddClientLoader,
3 | makeTypedUseLoaderData,
4 | makeTypedUsePatternLoaderData,
5 | makeTypedUseRouterData,
6 | type RiverRouteProps,
7 | } from "river.now/{{.UIVariant}}";
8 | import type { RiverLoader, RiverLoaderPattern, RiverRootData } from "./river.gen.ts";
9 |
10 | export type RouteProps = RiverRouteProps;
11 |
12 | export const useRouterData = makeTypedUseRouterData();
13 | export const useLoaderData = makeTypedUseLoaderData();
14 | export const addClientLoader = makeTypedAddClientLoader();
15 | export const usePatternLoaderData = makeTypedUsePatternLoaderData();
16 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/frontend_entry_tsx_preact_str.txt:
--------------------------------------------------------------------------------
1 | import { render } from "preact";
2 | import { getRootEl, initClient } from "river.now/client";
3 | import { App } from "./app.tsx";
4 |
5 | await initClient(() => {
6 | render(, getRootEl());
7 | });
8 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/frontend_entry_tsx_react_str.txt:
--------------------------------------------------------------------------------
1 | import { createRoot } from "react-dom/client";
2 | import { getRootEl, initClient } from "river.now/client";
3 | import { RiverProvider } from "river.now/react";
4 | import { App } from "./app.tsx";
5 |
6 | await initClient(() => {
7 | createRoot(getRootEl()).render(
8 |
9 |
10 | ,
11 | );
12 | });
13 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/frontend_entry_tsx_solid_str.txt:
--------------------------------------------------------------------------------
1 | import { getRootEl, initClient } from "river.now/client";
2 | import { render } from "solid-js/web";
3 | import { App } from "./app.tsx";
4 |
5 | await initClient(() => {
6 | render(() => , getRootEl());
7 | });
8 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/frontend_home_tsx_tmpl.txt:
--------------------------------------------------------------------------------
1 | import { useLoaderData, type RouteProps } from "./app_utils.ts";
2 |
3 | export function Home(props: RouteProps<"/">) {
4 | const loaderData = useLoaderData(props);
5 |
6 | return (
7 | <>
8 | Welcome to River!
9 | {loaderData{{.Accessor}}.Message}
10 | >
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/frontend_routes_ts_str.txt:
--------------------------------------------------------------------------------
1 | import type { RiverRoutes } from "river.now/client";
2 |
3 | declare const routes: RiverRoutes;
4 | export default routes;
5 |
6 | routes.Add("/", import("./home.tsx"), "Home");
7 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/gitignore_str.txt:
--------------------------------------------------------------------------------
1 | # Node
2 | node_modules
3 |
4 | # Wave
5 | __dist/static/*
6 | !__dist/static/.keep
7 | __dist/main*
8 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/main_critical_css_str.txt:
--------------------------------------------------------------------------------
1 | html {
2 | background-color: #333;
3 | color: #efefef;
4 | }
5 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/main_css_str.txt:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: monospace;
3 | }
4 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/package_json_str.txt:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "private": true,
4 | "scripts": {
5 | "dev": "go run ./__cmd/build --dev",
6 | "build": "go run ./__cmd/build"
7 | },
8 | "devDependencies": {}
9 | }
10 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/ts_config_json_tmpl.txt:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "ESNext",
5 | "moduleResolution": "Bundler",
6 | "forceConsistentCasingInFileNames": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "noUncheckedIndexedAccess": true,
12 | "verbatimModuleSyntax": true,
13 | "allowImportingTsExtensions": true,
14 | "jsx": "{{.TSConfigJSXVal}}",
15 | "jsxImportSource": "{{.TSConfigJSXImportSourceVal}}"
16 | },
17 | "exclude": ["node_modules"]
18 | }
19 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/vite_config_ts_tmpl.txt:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import {{.UIVariant}} from "{{.UIVitePlugin}}";
3 | import { riverVitePlugin } from "./frontend/river.gen.ts";
4 |
5 | export default defineConfig({
6 | plugins: [{{.UIVariant}}(), riverVitePlugin()],
7 | });
8 |
--------------------------------------------------------------------------------
/bootstrap/tmpls/wave_config_json_tmpl.txt:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "__dist/static/internal/schema.json",
3 | "Core": {
4 | "ConfigLocation": "wave.config.json",
5 | "DevBuildHook": "go run ./__cmd/build --dev --hook",
6 | "ProdBuildHook": "go run ./__cmd/build --hook",
7 | "MainAppEntry": "__cmd/app",
8 | "DistDir": "__dist",
9 | "StaticAssetDirs": {
10 | "Private": "backend/__static",
11 | "Public": "frontend/__static"
12 | },
13 | "CSSEntryFiles": {
14 | "Critical": "frontend/css/main.critical.css",
15 | "NonCritical": "frontend/css/main.css"
16 | },
17 | "PublicPathPrefix": "/public/"
18 | },
19 | "River": {
20 | "UIVariant": "{{.UIVariant}}",
21 | "HTMLTemplateLocation": "entry.go.html",
22 | "ClientEntry": "frontend/entry.tsx",
23 | "ClientRouteDefsFile": "frontend/routes.ts",
24 | "TSGenOutPath": "frontend/river.gen.ts",
25 | "BuildtimePublicURLFuncName": "getPublicURLBuildtime"
26 | },
27 | "Vite": {
28 | "JSPackageManagerBaseCmd": "{{.JSPackageManagerBaseCmd}}"
29 | },
30 | "Watch": {
31 | "HealthcheckEndpoint": "/healthz",
32 | "Include": []
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/bootstrap/utils.go:
--------------------------------------------------------------------------------
1 | package bootstrap
2 |
3 | import (
4 | "os"
5 | "strings"
6 | "text/template"
7 | )
8 |
9 | func (d *derivedOptions) tmplWriteMust(target, tmplStr string) {
10 | tmpl := template.Must(template.New(target).Parse(tmplStr))
11 | var sb strings.Builder
12 | if err := tmpl.Execute(&sb, d); err != nil {
13 | panic(err)
14 | }
15 | b := []byte(sb.String())
16 | if err := os.WriteFile(target, b, 0644); err != nil {
17 | panic(err)
18 | }
19 | }
20 |
21 | func strWriteMust(target string, content string) {
22 | if err := os.WriteFile(target, []byte(content), 0644); err != nil {
23 | panic(err)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/river-now/river
2 |
3 | go 1.24.0
4 |
5 | require (
6 | github.com/bmatcuk/doublestar/v4 v4.8.1
7 | github.com/evanw/esbuild v0.25.3
8 | github.com/fsnotify/fsnotify v1.9.0
9 | github.com/gorilla/websocket v1.5.3
10 | github.com/joho/godotenv v1.5.1
11 | golang.org/x/crypto v0.37.0
12 | golang.org/x/net v0.39.0
13 | golang.org/x/sync v0.13.0
14 | golang.org/x/term v0.31.0
15 | )
16 |
17 | require golang.org/x/sys v0.32.0 // indirect
18 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
2 | github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
3 | github.com/evanw/esbuild v0.25.3 h1:4JKyUsm/nHDhpxis4IyWXAi8GiyTwG1WdEp6OhGVE8U=
4 | github.com/evanw/esbuild v0.25.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
5 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
6 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
7 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
8 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
9 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
10 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
11 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
12 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
13 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
14 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
15 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
16 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
17 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
18 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
19 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
20 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
21 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
22 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/client/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | addBuildIDListener,
3 | addLocationListener,
4 | addRouteChangeListener,
5 | addStatusListener,
6 | applyScrollState,
7 | getBuildID,
8 | getHistoryInstance,
9 | getLocation,
10 | getPrefetchHandlers,
11 | getRootEl,
12 | getStatus,
13 | hmrRunClientLoaders,
14 | initClient,
15 | makeLinkOnClickFn,
16 | navigate,
17 | type RouteChangeEvent,
18 | revalidate,
19 | type StatusEvent,
20 | submit,
21 | } from "./src/client.ts";
22 | export {
23 | makeFinalLinkProps,
24 | type RiverLinkPropsBase,
25 | type RiverRouteGeneric,
26 | type RiverRoutePropsGeneric,
27 | type RiverUntypedLoader,
28 | type UseRouterDataFunction,
29 | } from "./src/impl_helpers.ts";
30 | export { getRouterData, internal_RiverClientGlobal } from "./src/river_ctx.ts";
31 | export type { RiverRoutes } from "./src/route_def_helpers.ts";
32 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/client/src/redirects.ts:
--------------------------------------------------------------------------------
1 | import { getHrefDetails, getIsGETRequest, type HrefDetails } from "river.now/kit/url";
2 | import { LogInfo } from "./utils.ts";
3 |
4 | export type RedirectData = { href: string; hrefDetails: HrefDetails } & (
5 | | {
6 | status: "did";
7 | }
8 | | {
9 | status: "should";
10 | shouldRedirectStrategy: "hard" | "soft";
11 | latestBuildID: string;
12 | }
13 | );
14 |
15 | export function getBuildIDFromResponse(response: Response | undefined) {
16 | return response?.headers.get("X-River-Build-Id") || "";
17 | }
18 |
19 | export function parseFetchResponseForRedirectData(
20 | reqInit: RequestInit,
21 | res: Response,
22 | ): RedirectData | null {
23 | const latestBuildID = getBuildIDFromResponse(res);
24 |
25 | const riverReloadTarget = res.headers.get("X-River-Reload");
26 | if (riverReloadTarget) {
27 | const newURL = new URL(riverReloadTarget, window.location.href);
28 | const hrefDetails = getHrefDetails(newURL.href);
29 | if (!hrefDetails.isHTTP) {
30 | return null;
31 | }
32 |
33 | return {
34 | hrefDetails,
35 | status: "should",
36 | href: riverReloadTarget,
37 | shouldRedirectStrategy: "hard",
38 | latestBuildID,
39 | };
40 | }
41 |
42 | if (res.redirected) {
43 | const newURL = new URL(res.url, window.location.href);
44 | const hrefDetails = getHrefDetails(newURL.href);
45 | if (!hrefDetails.isHTTP) {
46 | return null;
47 | }
48 |
49 | const isCurrent = newURL.href === window.location.href;
50 | if (isCurrent) {
51 | return { hrefDetails, status: "did", href: newURL.href };
52 | }
53 |
54 | const wasGETRequest = getIsGETRequest(reqInit);
55 | if (!wasGETRequest) {
56 | LogInfo("Not a GET request. No way to handle.");
57 | return null;
58 | }
59 |
60 | return {
61 | hrefDetails,
62 | status: "should",
63 | href: newURL.href,
64 | shouldRedirectStrategy: hrefDetails.isInternal ? "soft" : "hard",
65 | latestBuildID,
66 | };
67 | }
68 |
69 | const clientRedirectHeader = res.headers.get("X-Client-Redirect");
70 |
71 | if (!clientRedirectHeader) {
72 | return null;
73 | }
74 |
75 | const newURL = new URL(clientRedirectHeader, window.location.href);
76 | const hrefDetails = getHrefDetails(newURL.href);
77 | if (!hrefDetails.isHTTP) {
78 | return null;
79 | }
80 |
81 | return {
82 | hrefDetails,
83 | status: "should",
84 | href: hrefDetails.absoluteURL,
85 | shouldRedirectStrategy: hrefDetails.isInternal ? "soft" : "hard",
86 | latestBuildID,
87 | };
88 | }
89 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/client/src/river_ctx.ts:
--------------------------------------------------------------------------------
1 | export type HeadEl = {
2 | tag?: string;
3 | attributesKnownSafe?: Record;
4 | booleanAttributes?: Array;
5 | dangerousInnerHTML?: string;
6 | };
7 |
8 | type Meta = {
9 | title: HeadEl | null | undefined;
10 | metaHeadEls: Array | null | undefined;
11 | restHeadEls: Array | null | undefined;
12 | };
13 |
14 | type shared = {
15 | outermostError?: string;
16 | outermostErrorIdx?: number;
17 | errorExportKey?: string;
18 |
19 | matchedPatterns: Array;
20 | loadersData: Array;
21 | importURLs: Array;
22 | exportKeys: Array;
23 | hasRootData: boolean;
24 |
25 | params: Record;
26 | splatValues: Array;
27 |
28 | buildID: string;
29 |
30 | activeComponents: Array | null;
31 | activeErrorBoundary?: any;
32 | };
33 |
34 | export type GetRouteDataOutput = Omit &
35 | Meta & {
36 | deps: Array;
37 | cssBundles: Array;
38 | };
39 |
40 | export const RIVER_SYMBOL = Symbol.for("__river_internal__");
41 |
42 | export type RouteErrorComponent = (props: { error: string }) => any;
43 |
44 | export type RiverClientGlobal = shared & {
45 | isDev: boolean;
46 | viteDevURL: string;
47 | publicPathPrefix: string;
48 | isTouchDevice: boolean;
49 | patternToWaitFnMap: Record<
50 | string,
51 | (props: ReturnType & { loaderData: any }) => Promise
52 | >;
53 | clientLoadersData: Array;
54 | defaultErrorBoundary: RouteErrorComponent;
55 | useViewTransitions: boolean;
56 | };
57 |
58 | export function __getRiverClientGlobal() {
59 | const dangerousGlobalThis = globalThis as any;
60 | function get(key: K) {
61 | return dangerousGlobalThis[RIVER_SYMBOL][key] as RiverClientGlobal[K];
62 | }
63 | function set(
64 | key: K,
65 | value: V,
66 | ) {
67 | dangerousGlobalThis[RIVER_SYMBOL][key] = value;
68 | }
69 | return { get, set };
70 | }
71 |
72 | export const internal_RiverClientGlobal = __getRiverClientGlobal();
73 |
74 | // to debug ctx in browser, paste this:
75 | // const river_ctx = window[Symbol.for("__river_internal__")];
76 |
77 | export function getRouterData<
78 | T = any,
79 | P extends Record = Record,
80 | >() {
81 | const rootData: T = internal_RiverClientGlobal.get("hasRootData")
82 | ? internal_RiverClientGlobal.get("loadersData")[0]
83 | : null;
84 | return {
85 | buildID: internal_RiverClientGlobal.get("buildID") || "",
86 | matchedPatterns: internal_RiverClientGlobal.get("matchedPatterns") || [],
87 | splatValues: internal_RiverClientGlobal.get("splatValues") || [],
88 | params: (internal_RiverClientGlobal.get("params") || {}) as P,
89 | rootData,
90 | };
91 | }
92 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/client/src/route_def_helpers.ts:
--------------------------------------------------------------------------------
1 | type ImportPromise = Promise>;
2 | type Key = keyof Awaited;
3 |
4 | export type RiverRoutes = {
5 | Add: (
6 | pattern: string,
7 | importPromise: IP,
8 | componentKey: Key,
9 | errorBoundaryKey?: Key,
10 | ) => void;
11 | };
12 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/client/src/utils.ts:
--------------------------------------------------------------------------------
1 | /////////////////////////////////////////////////////////////////////
2 | // GENERAL UTILS
3 | /////////////////////////////////////////////////////////////////////
4 |
5 | export function isAbortError(error: unknown) {
6 | return error instanceof Error && error.name === "AbortError";
7 | }
8 |
9 | export function LogInfo(message?: any, ...optionalParams: Array) {
10 | console.log("River:", message, ...optionalParams);
11 | }
12 |
13 | export function LogError(message?: any, ...optionalParams: Array) {
14 | console.error("River:", message, ...optionalParams);
15 | }
16 |
17 | export function Panic(msg?: string): never {
18 | LogError("Panic");
19 | throw new Error(msg ?? "panic");
20 | }
21 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../tsconfig.base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/preact/index.tsx:
--------------------------------------------------------------------------------
1 | export {
2 | makeTypedAddClientLoader,
3 | makeTypedUseLoaderData,
4 | makeTypedUsePatternLoaderData,
5 | makeTypedUseRouterData,
6 | type RiverRoute,
7 | type RiverRouteProps,
8 | usePatternClientLoaderData,
9 | } from "./src/helpers.ts";
10 | export { RiverLink } from "./src/link.tsx";
11 | export { location, RiverRootOutlet } from "./src/preact.tsx";
12 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/preact/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "preact/hooks";
2 | import type { JSX } from "preact/jsx-runtime";
3 | import {
4 | type getRouterData,
5 | internal_RiverClientGlobal,
6 | type RiverRouteGeneric,
7 | type RiverRoutePropsGeneric,
8 | type RiverUntypedLoader,
9 | type UseRouterDataFunction,
10 | } from "river.now/client";
11 | import { clientLoadersData, loadersData, routerData } from "./preact.tsx";
12 |
13 | export type RiverRouteProps<
14 | Loader extends RiverUntypedLoader = RiverUntypedLoader,
15 | Pattern extends Loader["pattern"] = string,
16 | > = RiverRoutePropsGeneric;
17 |
18 | export type RiverRoute<
19 | Loader extends RiverUntypedLoader = RiverUntypedLoader,
20 | Pattern extends Loader["pattern"] = string,
21 | > = RiverRouteGeneric;
22 |
23 | export function makeTypedUseRouterData<
24 | OuterLoader extends RiverUntypedLoader,
25 | RootData,
26 | >() {
27 | return (() => {
28 | return routerData.value;
29 | }) as UseRouterDataFunction;
30 | }
31 |
32 | export function makeTypedUseLoaderData() {
33 | return function useLoaderData<
34 | Props extends RiverRouteProps,
35 | LoaderData = Extract<
36 | Loader,
37 | { pattern: Props["__phantom_pattern"] }
38 | >["phantomOutputType"],
39 | >(props: Props): LoaderData {
40 | return loadersData.value[props.idx];
41 | };
42 | }
43 |
44 | export function makeTypedUsePatternLoaderData() {
45 | return function usePatternData(
46 | pattern: Pattern,
47 | ): Extract["phantomOutputType"] | undefined {
48 | const idx = useMemo(() => {
49 | return routerData.value.matchedPatterns.findIndex((p) => p === pattern);
50 | }, [pattern]);
51 |
52 | if (idx === -1) {
53 | return undefined;
54 | }
55 | return loadersData.value[idx];
56 | };
57 | }
58 |
59 | export function usePatternClientLoaderData(
60 | pattern: string,
61 | ): ClientLoaderData | undefined {
62 | const idx = useMemo(() => {
63 | return routerData.value.matchedPatterns.findIndex((p) => p === pattern);
64 | }, [pattern]);
65 |
66 | if (idx === -1) {
67 | return undefined;
68 | }
69 | return clientLoadersData.value[idx];
70 | }
71 |
72 | export function makeTypedAddClientLoader<
73 | OuterLoader extends RiverUntypedLoader,
74 | RootData,
75 | >() {
76 | const m = internal_RiverClientGlobal.get("patternToWaitFnMap");
77 |
78 | return function addClientLoader<
79 | Pattern extends OuterLoader["pattern"],
80 | Loader extends Extract,
81 | RouterData = ReturnType>,
82 | LoaderData = Loader["phantomOutputType"],
83 | T = any,
84 | >(p: Pattern, fn: (props: RouterData & { loaderData: LoaderData }) => Promise) {
85 | (m as any)[p] = fn;
86 |
87 | return function useClientLoaderData(
88 | props: RiverRouteProps,
89 | ): Awaited> {
90 | return clientLoadersData.value[props.idx];
91 | };
92 | };
93 | }
94 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/preact/src/link.tsx:
--------------------------------------------------------------------------------
1 | import { h, type JSX } from "preact";
2 | import { useMemo } from "preact/hooks";
3 | import { makeFinalLinkProps, type RiverLinkPropsBase } from "river.now/client";
4 |
5 | export function RiverLink(
6 | props: JSX.HTMLAttributes &
7 | RiverLinkPropsBase<
8 | (e: JSX.TargetedMouseEvent) => void | Promise
9 | >,
10 | ) {
11 | const finalLinkProps = useMemo(() => makeFinalLinkProps(props), [props]);
12 |
13 | return h(
14 | "a",
15 | {
16 | "data-external": finalLinkProps.dataExternal,
17 | ...(props as any),
18 | onPointerEnter: finalLinkProps.onPointerEnter,
19 | onFocus: finalLinkProps.onFocus,
20 | onPointerLeave: finalLinkProps.onPointerLeave,
21 | onBlur: finalLinkProps.onBlur,
22 | onTouchCancel: finalLinkProps.onTouchCancel,
23 | onClick: finalLinkProps.onClick,
24 | },
25 | props.children,
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/preact/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "preact"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/react/index.tsx:
--------------------------------------------------------------------------------
1 | export {
2 | makeTypedAddClientLoader,
3 | makeTypedUseLoaderData,
4 | makeTypedUsePatternLoaderData,
5 | makeTypedUseRouterData,
6 | type RiverRoute,
7 | type RiverRouteProps,
8 | usePatternClientLoaderData,
9 | } from "./src/helpers.ts";
10 | export { RiverLink } from "./src/link.tsx";
11 | export { RiverProvider, RiverRootOutlet, useLocation } from "./src/react.tsx";
12 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/react/src/link.tsx:
--------------------------------------------------------------------------------
1 | import { type ComponentProps, useMemo } from "react";
2 | import { makeFinalLinkProps, type RiverLinkPropsBase } from "river.now/client";
3 |
4 | export function RiverLink(
5 | props: ComponentProps<"a"> &
6 | RiverLinkPropsBase<
7 | (e: React.MouseEvent) => void | Promise
8 | >,
9 | ) {
10 | const finalLinkProps = useMemo(() => makeFinalLinkProps(props), [props]);
11 |
12 | return (
13 |
24 | {props.children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "jsx": "react-jsx"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/solid/index.tsx:
--------------------------------------------------------------------------------
1 | export {
2 | makeTypedAddClientLoader,
3 | makeTypedUseLoaderData,
4 | makeTypedUsePatternLoaderData,
5 | makeTypedUseRouterData,
6 | type RiverRoute,
7 | type RiverRouteProps,
8 | usePatternClientLoaderData,
9 | } from "./src/helpers.ts";
10 | export { RiverLink } from "./src/link.tsx";
11 | export { location, RiverRootOutlet } from "./src/solid.tsx";
12 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/solid/src/link.tsx:
--------------------------------------------------------------------------------
1 | import { makeFinalLinkProps, type RiverLinkPropsBase } from "river.now/client";
2 | import { createMemo, type JSX } from "solid-js";
3 |
4 | export function RiverLink(
5 | props: JSX.AnchorHTMLAttributes &
6 | RiverLinkPropsBase["onClick"]>,
7 | ) {
8 | const finalLinkProps = createMemo(() => makeFinalLinkProps(props));
9 |
10 | return (
11 |
22 | {props.children}
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/internal/framework/_typescript/solid/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "jsx": "preserve",
5 | "jsxImportSource": "solid-js"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/internal/framework/get_deps.go:
--------------------------------------------------------------------------------
1 | package framework
2 |
3 | import "github.com/river-now/river/kit/matcher"
4 |
5 | func (h *River) getDeps(_matches []*matcher.Match) []string {
6 | var deps []string
7 | seen := make(map[string]struct{}, len(_matches))
8 | handleDeps := func(src []string) {
9 | for _, d := range src {
10 | if _, ok := seen[d]; !ok {
11 | deps = append(deps, d)
12 | seen[d] = struct{}{}
13 | }
14 | }
15 | }
16 | if h._clientEntryDeps != nil {
17 | handleDeps(h._clientEntryDeps)
18 | }
19 | for _, match := range _matches {
20 | path := h._paths[match.OriginalPattern()]
21 | if path == nil {
22 | continue
23 | }
24 | handleDeps(path.Deps)
25 | }
26 | return deps
27 | }
28 |
29 | // order matters
30 | func (h *River) getCSSBundles(deps []string) []string {
31 | cssBundles := make([]string, 0, len(deps))
32 | // first, client entry CSS
33 | if x, exists := h._depToCSSBundleMap[h._clientEntryOut]; exists {
34 | cssBundles = append(cssBundles, x)
35 | }
36 | // then all downstream deps
37 | for _, dep := range deps {
38 | if x, exists := h._depToCSSBundleMap[dep]; exists {
39 | cssBundles = append(cssBundles, x)
40 | }
41 | }
42 | return cssBundles
43 | }
44 |
--------------------------------------------------------------------------------
/internal/framework/river_core.go:
--------------------------------------------------------------------------------
1 | package framework
2 |
3 | import (
4 | "html/template"
5 | "io/fs"
6 | "net/http"
7 | "sync"
8 |
9 | "github.com/river-now/river/kit/colorlog"
10 | "github.com/river-now/river/kit/headels"
11 | "github.com/river-now/river/kit/htmlutil"
12 | "github.com/river-now/river/kit/mux"
13 | "github.com/river-now/river/wave"
14 | )
15 |
16 | const (
17 | RiverSymbolStr = "__river_internal__"
18 | )
19 |
20 | var Log = colorlog.New("river")
21 |
22 | type RouteType = string
23 |
24 | var RouteTypes = struct {
25 | Loader RouteType
26 | Query RouteType
27 | Mutation RouteType
28 | NotFound RouteType
29 | }{
30 | Loader: "loader",
31 | Query: "query",
32 | Mutation: "mutation",
33 | NotFound: "not-found",
34 | }
35 |
36 | type Path struct {
37 | NestedRoute mux.AnyNestedRoute `json:"-"`
38 |
39 | // both stages one and two
40 | OriginalPattern string `json:"originalPattern"`
41 | SrcPath string `json:"srcPath"`
42 | ExportKey string `json:"exportKey"`
43 | ErrorExportKey string `json:"errorExportKey,omitempty"`
44 |
45 | // stage two only
46 | OutPath string `json:"outPath,omitempty"`
47 | Deps []string `json:"deps,omitempty"`
48 | }
49 |
50 | type UIVariant string
51 |
52 | var UIVariants = struct {
53 | React UIVariant
54 | Preact UIVariant
55 | Solid UIVariant
56 | }{
57 | React: "react",
58 | Preact: "preact",
59 | Solid: "solid",
60 | }
61 |
62 | type River struct {
63 | Wave *wave.Wave
64 | GetDefaultHeadEls func(r *http.Request) ([]*htmlutil.Element, error)
65 | GetHeadElUniqueRules func() *headels.HeadEls
66 | GetRootTemplateData func(r *http.Request) (map[string]any, error)
67 |
68 | mu sync.RWMutex
69 | _isDev bool
70 | _paths map[string]*Path
71 | _clientEntrySrc string
72 | _clientEntryOut string
73 | _clientEntryDeps []string
74 | _buildID string
75 | _depToCSSBundleMap map[string]string
76 | _rootTemplate *template.Template
77 | _privateFS fs.FS
78 | }
79 |
--------------------------------------------------------------------------------
/internal/framework/vite_cmd.go:
--------------------------------------------------------------------------------
1 | package framework
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | "path/filepath"
8 | )
9 |
10 | func (h *River) PostViteProdBuild() error {
11 | // Must come after Vite -- only needed in prod (the stage "one" version is fine in dev)
12 | pf, err := h.toPathsFile_StageTwo()
13 | if err != nil {
14 | Log.Error(fmt.Sprintf("error converting paths to paths file: %s", err))
15 | return err
16 | }
17 |
18 | pathsAsJSON, err := json.MarshalIndent(pf, "", "\t")
19 |
20 | if err != nil {
21 | Log.Error(fmt.Sprintf("error marshalling paths to JSON: %s", err))
22 | return err
23 | }
24 |
25 | pathsJSONOut_StageTwo := filepath.Join(
26 | h.Wave.GetStaticPrivateOutDir(),
27 | "river_out",
28 | RiverPathsStageTwoJSONFileName,
29 | )
30 | err = os.WriteFile(pathsJSONOut_StageTwo, pathsAsJSON, os.ModePerm)
31 | if err != nil {
32 | Log.Error(fmt.Sprintf("error writing paths to disk: %s", err))
33 | return err
34 | }
35 |
36 | return nil
37 | }
38 |
--------------------------------------------------------------------------------
/internal/junk-drawer/playground/embedded/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | )
7 |
8 | // If embedded direct with no json tag: just as though it were top-level
9 | // If embedded pointer with no json tag: just as though top-level, but fields optional
10 | // If embedded direct with json tag: tag becomes a required root field under which its fields are nested
11 | // If embedded pointer with json tag: tag becomes an optional root field under which its fields are nested
12 |
13 | type Base struct{ Name string }
14 | type BasePtr struct{ NamePtr string }
15 | type Wrapper struct {
16 | BuiltIn string
17 | Base
18 | *BasePtr
19 | }
20 | type WrapperWithJSONTags struct {
21 | BuiltIn string
22 | Base `json:"base"`
23 | *BasePtr `json:"basePtr"`
24 | }
25 |
26 | func main() {
27 | w := Wrapper{}
28 | wJsonTags := WrapperWithJSONTags{}
29 |
30 | jsonBytes_w, _ := json.Marshal(w)
31 | fmt.Println(string(jsonBytes_w))
32 |
33 | jsonBytes_wJsonTags, _ := json.Marshal(wJsonTags)
34 | fmt.Println(string(jsonBytes_wJsonTags))
35 | }
36 |
--------------------------------------------------------------------------------
/internal/junk-drawer/playground/matcher/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/river-now/river/kit/matcher"
7 | )
8 |
9 | var m = matcher.New(&matcher.Options{
10 | ExplicitIndexSegment: "_index",
11 | DynamicParamPrefixRune: '$',
12 | })
13 |
14 | var rps = registerPatterns([]string{"/", "/$user", "/$user/*", "/posts"})
15 |
16 | func main() {
17 | fmt.Println("REGISTERED PATTERNS")
18 | for _, rp := range rps {
19 | fmt.Printf("Clean: '%s', Original: '%s'\n", rp.NormalizedPattern(), rp.OriginalPattern())
20 | for _, seg := range rp.NormalizedSegments() {
21 | fmt.Println(seg)
22 | }
23 | }
24 |
25 | fmt.Println()
26 |
27 | var matches []*matcher.BestMatch
28 | var ok bool
29 | // matches, ok = m.FindNestedMatches("/")
30 | match, ok := m.FindBestMatch("/bob")
31 | if ok {
32 | matches = append(matches, match)
33 | }
34 |
35 | if !ok {
36 | fmt.Println("No match found")
37 | return
38 | } else {
39 | fmt.Println("MATCHES:", len(matches))
40 | }
41 |
42 | for i, match := range matches {
43 | fmt.Println()
44 | fmt.Println("Match", i+1)
45 | fmt.Println()
46 | if len(match.Params) > 0 {
47 | fmt.Println("Params:", match.Params)
48 | }
49 |
50 | fmt.Println("SplatValues:", match.SplatValues)
51 | fmt.Printf("Clean: '%s', Original: '%s'\n", match.NormalizedPattern(), match.RegisteredPattern.OriginalPattern())
52 | fmt.Println()
53 | }
54 | }
55 |
56 | /////////////////////////////////////////////////////////////////////
57 | /////// UTILS
58 | /////////////////////////////////////////////////////////////////////
59 |
60 | func registerPatterns(ps []string) []*matcher.RegisteredPattern {
61 | var rps []*matcher.RegisteredPattern
62 | for _, p := range ps {
63 | rps = append(rps, m.RegisterPattern(p))
64 | }
65 | return rps
66 | }
67 |
--------------------------------------------------------------------------------
/internal/junk-drawer/playground/nested/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/river-now/river/kit/mux"
10 | "github.com/river-now/river/kit/tasks"
11 | )
12 |
13 | var tasksRegistry = tasks.NewRegistry("jd")
14 |
15 | var r = mux.NewNestedRouter(&mux.NestedOptions{
16 | TasksRegistry: tasksRegistry,
17 | ExplicitIndexSegment: "_index",
18 | })
19 |
20 | func newLoaderTask[O any](f func(*mux.NestedReqData) (O, error)) *mux.TaskHandler[mux.None, O] {
21 | return mux.TaskHandlerFromFunc(tasksRegistry, f)
22 | }
23 |
24 | var AuthTask = newLoaderTask(func(rd *mux.NestedReqData) (int, error) {
25 | fmt.Println("running auth ...", rd.Request().URL, time.Now().UnixMilli())
26 | time.Sleep(1 * time.Second)
27 | fmt.Println("finishing auth ...", rd.Request().URL, time.Now().UnixMilli())
28 | return 123, nil
29 | })
30 |
31 | var AuthLarryTask = newLoaderTask(func(rd *mux.NestedReqData) (int, error) {
32 | fmt.Println("running auth larry ...", rd.Request().URL, time.Now().UnixMilli())
33 | time.Sleep(1 * time.Second)
34 | fmt.Println("finishing auth larry ...", rd.Request().URL, time.Now().UnixMilli())
35 | // return 24892498, nil
36 | return 0, errors.New("auth larry error")
37 | })
38 |
39 | var AuthLarryIDTask = newLoaderTask(func(rd *mux.NestedReqData) (string, error) {
40 | fmt.Println("running auth larry :id ...", rd.Request().URL, time.Now().UnixMilli())
41 | time.Sleep(1 * time.Second)
42 | fmt.Println("finishing auth larry :id ...", rd.Params()["id"], time.Now().UnixMilli())
43 | return "*** Larry has an ID of " + rd.Params()["id"], nil
44 | })
45 |
46 | func registerLoader[O any](pattern string, taskHandler *mux.TaskHandler[mux.None, O]) {
47 | mux.RegisterNestedTaskHandler(r, pattern, taskHandler)
48 | }
49 |
50 | func initRoutes() {
51 | registerLoader("/auth", AuthTask)
52 | registerLoader("/auth/larry", AuthLarryTask)
53 | registerLoader("/auth/larry/:id", AuthLarryIDTask)
54 | }
55 |
56 | func main() {
57 | initRoutes()
58 |
59 | req, _ := http.NewRequest("GET", "/auth/larry/12879", nil)
60 |
61 | tasksCtx := tasksRegistry.NewCtxFromRequest(req)
62 |
63 | results, _ := mux.FindNestedMatchesAndRunTasks(r, tasksCtx, req)
64 |
65 | fmt.Println()
66 |
67 | fmt.Println("results.Params", results.Params)
68 | fmt.Println("results.SplatValues", results.SplatValues)
69 |
70 | for _, v := range results.Slice {
71 | fmt.Println()
72 |
73 | fmt.Println("result: ", v.Pattern())
74 |
75 | if v.OK() {
76 | fmt.Println("Data: ", v.Data())
77 | } else {
78 | fmt.Println("Err : ", v.Err())
79 | }
80 | }
81 |
82 | fmt.Println()
83 | }
84 |
--------------------------------------------------------------------------------
/internal/junk-drawer/playground/tasks/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 | "time"
8 |
9 | "github.com/river-now/river/kit/tasks"
10 | )
11 |
12 | var tasksRegistry = tasks.NewRegistry("jd")
13 |
14 | var (
15 | Auth = tasks.Register(tasksRegistry, auth)
16 | User = tasks.Register(tasksRegistry, user)
17 | User2 = tasks.Register(tasksRegistry, user2)
18 | Profile = tasks.Register(tasksRegistry, profile)
19 | )
20 |
21 | func main() {
22 | go func() {
23 | // every 1 second, print a new line
24 | for {
25 | fmt.Println()
26 | time.Sleep(1 * time.Second)
27 | }
28 | }()
29 |
30 | req, _ := http.NewRequest("GET", "http://localhost:8080", nil)
31 | c := tasksRegistry.NewCtxFromRequest(req)
32 |
33 | data, err := Profile.Prep(c, "32isdoghj").Get()
34 |
35 | fmt.Println("from main -- profile data:", data)
36 | fmt.Println("from main -- profile err:", err)
37 | }
38 |
39 | func auth(c *tasks.ArgNoInput) (int, error) {
40 | fmt.Println("running auth ...", time.Now().UnixMilli())
41 | // return 0, errors.New("auth error")
42 |
43 | time.Sleep(2 * time.Second)
44 |
45 | fmt.Println("auth done", time.Now().UnixMilli())
46 | return 123, nil
47 | }
48 |
49 | func user(c *tasks.Arg[string]) (string, error) {
50 | user_id := c.Input
51 | fmt.Println("running user ...", user_id, time.Now().UnixMilli())
52 | // time.Sleep(500 * time.Millisecond)
53 | // c.Cancel()
54 | token, _ := Auth.PrepNoInput(c.TasksCtx).Get()
55 | fmt.Println("user retrieved token", token)
56 |
57 | time.Sleep(2 * time.Second)
58 |
59 | fmt.Println("user done", time.Now().UnixMilli())
60 | return fmt.Sprintf("user-%d", token), nil
61 | }
62 |
63 | func user2(c *tasks.Arg[string]) (string, error) {
64 | fmt.Println("running user2 ...", time.Now().UnixMilli())
65 | token, _ := Auth.PrepNoInput(c.TasksCtx).Get()
66 | fmt.Println("user2 retrieved token", token)
67 |
68 | time.Sleep(2 * time.Second)
69 |
70 | fmt.Println("user2 done", time.Now().UnixMilli())
71 | return fmt.Sprintf("user2-%d", token), nil
72 | }
73 |
74 | func profile(c *tasks.Arg[string]) (string, error) {
75 | user_id := c.Input
76 | fmt.Println("running profile...", time.Now().UnixMilli())
77 | user := User.Prep(c.TasksCtx, user_id)
78 | user2 := User2.Prep(c.TasksCtx, user_id)
79 |
80 | fmt.Println("profile running user, user2 in parallel")
81 | if ok := c.ParallelPreload(user, user2); !ok {
82 | return "", errors.New("user error")
83 | }
84 | fmt.Println("profile running user, user2 in parallel done")
85 | userData, _ := user.Get()
86 | user2Data, _ := user2.Get()
87 | fmt.Println("profile user, user2 data", userData, user2Data)
88 |
89 | time.Sleep(2 * time.Second)
90 | fmt.Println("profile done", time.Now().UnixMilli(), userData, user2Data)
91 | return "profile", nil
92 | }
93 |
--------------------------------------------------------------------------------
/internal/junk-drawer/playground/tstyper/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | )
7 |
8 | type Base struct {
9 | X string `json:"x"`
10 | }
11 |
12 | func (b Base) TSType() map[string]string { return map[string]string{"X": "asdf"} }
13 |
14 | type BaseWithPtrMethod struct {
15 | X string `json:"x"`
16 | }
17 |
18 | func (b *BaseWithPtrMethod) TSType() map[string]string { return map[string]string{"X": "asdf"} }
19 |
20 | type Base_Wrapped struct{ Base }
21 | type Base_WrappedPtr struct{ *Base }
22 | type BaseWithPtrMethod_Wrapped struct{ BaseWithPtrMethod }
23 | type BaseWithPtrMethod_WrappedPtr struct{ *BaseWithPtrMethod }
24 |
25 | type TSTyper interface {
26 | TSType() map[string]string
27 | }
28 |
29 | func implementsTSTyper(t reflect.Type) bool {
30 | if t.Implements(reflect.TypeOf((*TSTyper)(nil)).Elem()) {
31 | return true
32 | }
33 | if t.Kind() != reflect.Ptr && reflect.PointerTo(t).Implements(reflect.TypeOf((*TSTyper)(nil)).Elem()) {
34 | return true
35 | }
36 | return false
37 | }
38 |
39 | func main() {
40 | run(Base{})
41 | run(&Base{})
42 | fmt.Println()
43 |
44 | run(BaseWithPtrMethod{})
45 | run(&BaseWithPtrMethod{})
46 | fmt.Println()
47 |
48 | run(Base_Wrapped{})
49 | run(&Base_Wrapped{})
50 | fmt.Println()
51 |
52 | run(Base_WrappedPtr{})
53 | run(&Base_WrappedPtr{})
54 | fmt.Println()
55 |
56 | run(BaseWithPtrMethod_Wrapped{})
57 | run(&BaseWithPtrMethod_Wrapped{})
58 | fmt.Println()
59 |
60 | run(BaseWithPtrMethod_WrappedPtr{})
61 | run(&BaseWithPtrMethod_WrappedPtr{})
62 | }
63 |
64 | func run(x any) {
65 | reflectType := reflect.TypeOf(x)
66 | name := reflectType.Name()
67 | if reflectType.Kind() == reflect.Ptr {
68 | name = "*" + reflectType.Elem().Name()
69 | }
70 | if implementsTSTyper(reflectType) {
71 | fmt.Println(name, " -- yes", getTSTypeMap(reflectType))
72 | } else {
73 | fmt.Println(name, " -- no")
74 | }
75 | }
76 |
77 | func getTSTypeMap(t reflect.Type) map[string]string {
78 | var instance reflect.Value
79 | if t.Kind() == reflect.Ptr {
80 | instance = reflect.New(t.Elem())
81 | } else {
82 | instance = reflect.New(t)
83 | }
84 | initializeEmbeddedPointers(instance)
85 | if t.Kind() == reflect.Ptr {
86 | return instance.Interface().(TSTyper).TSType()
87 | } else {
88 | return instance.Interface().(TSTyper).TSType()
89 | }
90 | }
91 |
92 | func initializeEmbeddedPointers(v reflect.Value) {
93 | if v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct {
94 | elem := v.Elem()
95 | typ := elem.Type()
96 | for i := range elem.NumField() {
97 | field := elem.Field(i)
98 | fieldType := typ.Field(i)
99 | if fieldType.Anonymous && field.Kind() == reflect.Ptr && field.IsNil() {
100 | newValue := reflect.New(field.Type().Elem())
101 | field.Set(newValue)
102 | initializeEmbeddedPointers(newValue)
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/internal/junk-drawer/playground/validate/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/river-now/river/kit/validate"
8 | )
9 |
10 | type FirstName string
11 | type LastName string
12 |
13 | func (ln LastName) Validate() error {
14 | return errors.New("asdf")
15 | }
16 |
17 | type Job struct {
18 | Employer string
19 | Title string
20 | }
21 |
22 | type Email struct {
23 | EmailAddress *string
24 | }
25 |
26 | type Person struct {
27 | FirstName
28 | LastName
29 | *Job
30 | Email
31 | }
32 |
33 | func (p *Person) Validate() error {
34 | v := validate.Object(p)
35 | v.Required("FirstName")
36 | v.Required("LastName")
37 | v.Required("Employer")
38 | v.Required("Title")
39 | v.Required("EmailAddress")
40 | return v.Error()
41 | }
42 |
43 | func main() {
44 | // lastName := LastName("Cook")
45 | // // emailAddress := "bob@bob.com
46 |
47 | // p := &Person{
48 | // FirstName: "Samuel",
49 | // LastName: lastName,
50 | // Job: &Job{
51 | // Employer: "Jim",
52 | // Title: "",
53 | // },
54 | // Email: Email{
55 | // // EmailAddress: &emailAddress,
56 | // },
57 | // }
58 |
59 | // err := p.Validate()
60 |
61 | // if err != nil {
62 | // fmt.Println(err)
63 | // } else {
64 | // fmt.Println("OK")
65 | // }
66 |
67 | // //////////////
68 |
69 | // var x LastName = "Cook"
70 | // err = validate.Any("x", x).Required().Error()
71 | // if err != nil {
72 | // fmt.Println("2", err)
73 | // } else {
74 | // fmt.Println("2 OK")
75 | // }
76 |
77 | // err := validate.Any("int", 0).Required().Error()
78 | // if err != nil {
79 | // fmt.Println("3", err)
80 | // } else {
81 | // fmt.Println("3 OK")
82 | // }
83 |
84 | var m map[string]any
85 | fmt.Println("m", m, m == nil)
86 | err := validate.Object(m).Error()
87 | fmt.Println(err)
88 | }
89 |
90 | // type Person map[string]string
91 |
92 | // func (p *Person) Validate() error {
93 | // v := validate.Object(p)
94 | // v.Required("FirstName").Min(20)
95 | // v.Required("LastName")
96 | // return v.Error()
97 | // }
98 |
99 | // func main() {
100 | // p := &Person{
101 | // "FirstName": "Samuel",
102 | // "LastName": "Cook",
103 | // }
104 |
105 | // err := p.Validate()
106 |
107 | // if err != nil {
108 | // fmt.Println(err)
109 | // } else {
110 | // fmt.Println("OK")
111 | // }
112 | // }
113 |
--------------------------------------------------------------------------------
/internal/scripts/_typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../../tsconfig.base.json"
3 | }
4 |
--------------------------------------------------------------------------------
/internal/scripts/buildts/build-solid.mjs:
--------------------------------------------------------------------------------
1 | import { build } from "esbuild";
2 | import { solidPlugin } from "esbuild-plugin-solid";
3 |
4 | await build({
5 | plugins: [solidPlugin()],
6 | sourcemap: "linked",
7 | target: "esnext",
8 | format: "esm",
9 | treeShaking: true,
10 | splitting: true,
11 | write: true,
12 | bundle: true,
13 | entryPoints: ["./internal/framework/_typescript/solid/index.tsx"],
14 | external: ["river.now", "solid-js"],
15 | outdir: "./npm_dist/internal/framework/_typescript/solid",
16 | tsconfig: "./internal/framework/_typescript/solid/tsconfig.json",
17 | });
18 |
--------------------------------------------------------------------------------
/internal/scripts/bumper/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/river-now/river/kit/scripts/bumper"
5 | )
6 |
7 | func main() {
8 | bumper.Run()
9 | }
10 |
--------------------------------------------------------------------------------
/internal/scripts/npm_bumper/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "strings"
6 |
7 | t "github.com/river-now/river/kit/cliutil"
8 | "github.com/river-now/river/kit/parseutil"
9 | )
10 |
11 | func main() {
12 | lines, versionLine, currentVersion := parseutil.PackageJSONFromFile("./package.json")
13 |
14 | // Show current tag
15 | t.Plain("current version: ")
16 | t.Green(currentVersion)
17 | t.NewLine()
18 |
19 | // Ask for new version
20 | t.Blue("what is the new version? ")
21 | version, err := t.NewReader().ReadString('\n')
22 | if err != nil {
23 | t.Exit("failed to read version", err)
24 | }
25 |
26 | trimmedVersion := strings.TrimSpace(version)
27 | if trimmedVersion == "" {
28 | t.Exit("version is empty", nil)
29 | }
30 |
31 | // Show new tag
32 | t.Plain("Result: ")
33 | t.Red(currentVersion)
34 | t.Plain(" --> ")
35 | t.Green(trimmedVersion)
36 | t.NewLine()
37 |
38 | // Ask for confirmation
39 | t.Blue("is this correct? ")
40 | t.RequireYes("aborted")
41 |
42 | lines[versionLine] = strings.Replace(lines[versionLine], currentVersion, trimmedVersion, 1)
43 |
44 | // Ask for write confirmation
45 | t.Blue("write new version ")
46 | t.Green(trimmedVersion)
47 | t.Blue(" to package.json? ")
48 | t.RequireYes("aborted")
49 |
50 | // Write the new version to the file
51 | if err = os.WriteFile("./package.json", []byte(strings.Join(lines, "\n")+"\n"), 0644); err != nil {
52 | t.Exit("failed to write file", err)
53 | }
54 |
55 | // Sanity check
56 | _, _, newCurrentVersion := parseutil.PackageJSONFromFile("./package.json")
57 | if newCurrentVersion != trimmedVersion {
58 | t.Exit("failed to update version", nil)
59 | }
60 |
61 | isPre := strings.Contains(newCurrentVersion, "pre")
62 |
63 | if isPre {
64 | t.Plain("pre-release version detected")
65 | t.NewLine()
66 | }
67 |
68 | // Ask whether to initiate a new build?
69 | t.Blue("emit a new build to ./npm_dist? ")
70 | t.RequireYes("aborted")
71 |
72 | cmd := t.Cmd("make", "npmbuild")
73 | t.MustRun(cmd, "npm dist build failed")
74 |
75 | // Ask for publish confirmation
76 | t.Blue("do you want to publish ")
77 | if isPre {
78 | t.Red("PRE release ")
79 | } else {
80 | t.Red("FINAL release ")
81 | }
82 | t.Green(trimmedVersion)
83 | t.Blue(" npm? ")
84 | t.RequireYes("aborted")
85 |
86 | cmd = t.Cmd("make", "tspublishpre")
87 | if !isPre {
88 | cmd = t.Cmd("make", "tspublishnonpre")
89 | }
90 |
91 | t.MustRun(cmd, "npm publish failed")
92 |
93 | t.Plain("npm publish done")
94 | t.NewLine()
95 | }
96 |
--------------------------------------------------------------------------------
/kit/_typescript/cookies/cookies.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Checks the client cookie for a specific name. Returns the value if
3 | * found, otherwise undefined. Does not do any encoding or decoding.
4 | */
5 | export function getClientCookie(name: string) {
6 | const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
7 | return match ? match[2] : undefined;
8 | }
9 |
10 | /**
11 | * Sets a client cookie with the specified name and value. The cookie
12 | * is set to expire in one year and is accessible to all paths on the
13 | * domain. The SameSite attribute is set to Lax. Does not do any
14 | * encoding or decoding.
15 | */
16 | export function setClientCookie(name: string, value: string) {
17 | document.cookie = `${name}=${value}; path=/; max-age=31536000; SameSite=Lax`;
18 | }
19 |
--------------------------------------------------------------------------------
/kit/_typescript/debounce/debounce.test.ts:
--------------------------------------------------------------------------------
1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2 | import { debounce } from "./debounce.ts";
3 |
4 | describe("debounce", () => {
5 | beforeEach(() => {
6 | vi.useFakeTimers();
7 | });
8 |
9 | afterEach(() => {
10 | vi.useRealTimers();
11 | vi.clearAllMocks();
12 | });
13 |
14 | it("delays the call by the specified time and resolves with return value", async () => {
15 | const spy = vi.fn((a: number, b: number) => a + b);
16 | const debounced = debounce(spy, 100);
17 |
18 | const resultPromise = debounced(1, 2);
19 | // not called immediately
20 | expect(spy).not.toHaveBeenCalled();
21 |
22 | // midway through delay
23 | vi.advanceTimersByTime(50);
24 | expect(spy).not.toHaveBeenCalled();
25 |
26 | // after full delay
27 | vi.advanceTimersByTime(50);
28 | await expect(resultPromise).resolves.toBe(3);
29 |
30 | expect(spy).toHaveBeenCalledTimes(1);
31 | expect(spy).toHaveBeenCalledWith(1, 2);
32 | });
33 |
34 | it("only the last call within the delay window executes", async () => {
35 | const spy = vi.fn((x: number) => x * 2);
36 | const debounced = debounce(spy, 100);
37 |
38 | // first call
39 | const p1 = debounced(5);
40 | // before it fires, call again
41 | vi.advanceTimersByTime(50);
42 | const p2 = debounced(6);
43 |
44 | // advance past delay
45 | vi.advanceTimersByTime(100);
46 |
47 | // only the second promise should resolve
48 | await expect(p2).resolves.toBe(12);
49 |
50 | // ensure original call was cancelled
51 | expect(spy).toHaveBeenCalledTimes(1);
52 | expect(spy).toHaveBeenCalledWith(6);
53 | });
54 |
55 | it("maintains correct `this` if wrapped in a method", async () => {
56 | const obj = {
57 | value: 10,
58 | getValue(add: number) {
59 | return this.value + add;
60 | },
61 | };
62 | // wrap as a method
63 | const debounced = debounce(obj.getValue.bind(obj), 30);
64 |
65 | const p = debounced(5);
66 | vi.advanceTimersByTime(30);
67 | await expect(p).resolves.toBe(15);
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/kit/_typescript/debounce/debounce.ts:
--------------------------------------------------------------------------------
1 | type Fn = (...args: Array) => any;
2 |
3 | export function debounce(
4 | fn: T,
5 | delayInMs: number,
6 | ): (...args: Parameters) => Promise>> {
7 | let timeoutID: number;
8 |
9 | return (...args: Parameters) => {
10 | return new Promise>>((resolve) => {
11 | clearTimeout(timeoutID);
12 | timeoutID = window.setTimeout(() => {
13 | resolve(fn(...args));
14 | }, delayInMs);
15 | });
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/kit/_typescript/fmt/fmt.ts:
--------------------------------------------------------------------------------
1 | export function prettyJSON(obj: any): string {
2 | return JSON.stringify(obj, null, 2);
3 | }
4 |
--------------------------------------------------------------------------------
/kit/_typescript/json/deep_equals.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Performs a deep equality comparison of two JSON-compatible values.
3 | * Handles null, undefined, primitives, arrays, and plain objects, recursively.
4 | * Does not support Maps, Sets, Functions, or other non-JSON types.
5 | */
6 | export function jsonDeepEquals(a: unknown, b: unknown): boolean {
7 | // Tautology
8 | if (a === b) {
9 | return true;
10 | }
11 |
12 | // If both were null or both were undefined, we would have early returned above.
13 | // So if either at this point is loosely null, we know they're not equal.
14 | if (a == null || b == null) {
15 | return false;
16 | }
17 |
18 | // If types are different, we know they're not equal.
19 | if (typeof a !== typeof b) {
20 | return false;
21 | }
22 |
23 | const aIsArray = Array.isArray(a);
24 | const bIsArray = Array.isArray(b);
25 |
26 | // If one is an array and the other is not, we know they're not equal.
27 | if (aIsArray !== bIsArray) {
28 | return false;
29 | }
30 |
31 | // Handle arrays
32 | if (aIsArray && bIsArray) {
33 | if (a.length !== b.length) {
34 | return false;
35 | }
36 | return a.every((item, index) => jsonDeepEquals(item, b[index]));
37 | }
38 |
39 | // Handle objects
40 | if (typeof a === "object" && typeof b === "object") {
41 | const aKeys = Object.keys(a as object);
42 | const bKeys = Object.keys(b as object);
43 |
44 | if (aKeys.length !== bKeys.length) {
45 | return false;
46 | }
47 |
48 | return aKeys.every((key) => {
49 | return Object.hasOwn(b, key) && jsonDeepEquals((a as any)[key], (b as any)[key]);
50 | });
51 | }
52 |
53 | return false;
54 | }
55 |
--------------------------------------------------------------------------------
/kit/_typescript/json/json.ts:
--------------------------------------------------------------------------------
1 | export { jsonDeepEquals } from "./deep_equals.ts";
2 | export { serializeToSearchParams } from "./search_param_serializer.ts";
3 | export { jsonStringifyStable } from "./stringify_stable.ts";
4 |
--------------------------------------------------------------------------------
/kit/_typescript/json/search_param_serializer.ts:
--------------------------------------------------------------------------------
1 | export function serializeToSearchParams(obj: Record): URLSearchParams {
2 | const params = new URLSearchParams();
3 |
4 | function appendValue(key: string, value: any) {
5 | if (value === null || value === undefined) {
6 | params.append(key, "");
7 | return;
8 | }
9 |
10 | if (Array.isArray(value)) {
11 | if (value.length === 0) {
12 | params.append(key, "");
13 | } else {
14 | for (const item of value) {
15 | appendValue(key, item);
16 | }
17 | }
18 | return;
19 | }
20 |
21 | if (typeof value === "object") {
22 | const entries = Object.entries(value);
23 | if (entries.length === 0) {
24 | params.append(key, "");
25 | } else {
26 | // Sort nested keys alphabetically
27 | entries.sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
28 | for (const [subKey, subValue] of entries) {
29 | const newKey = key ? `${key}.${subKey}` : subKey;
30 | appendValue(newKey, subValue);
31 | }
32 | }
33 | return;
34 | }
35 |
36 | params.append(key, String(value));
37 | }
38 |
39 | if (typeof obj === "object" && obj !== null) {
40 | // Sort top-level keys alphabetically
41 | const entries = Object.entries(obj);
42 | entries.sort(([keyA], [keyB]) => keyA.localeCompare(keyB));
43 | for (const [key, value] of entries) {
44 | appendValue(key, value);
45 | }
46 | }
47 |
48 | return params;
49 | }
50 |
--------------------------------------------------------------------------------
/kit/_typescript/json/stringify_stable.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Deterministically serializes a JSON-compatible value to a stable string.
3 | * Throws if it detects a circular reference. Does not support Maps, Sets,
4 | * Functions, or other non-JSON types.
5 | */
6 | export function jsonStringifyStable(input: unknown): string {
7 | // First stabilize the structure, then JSON.stringify
8 | const stabilized = stabilizeStructure(input, new WeakSet());
9 |
10 | return JSON.stringify(stabilized);
11 | }
12 |
13 | function stabilizeStructure(value: unknown, visited: WeakSet