diff --git a/www/deno.json b/www/deno.json index ebd516898..728d29f23 100644 --- a/www/deno.json +++ b/www/deno.json @@ -1,6 +1,6 @@ { "tasks": { - "dev": "deno run -A lib/watch.ts components docs lib plugins routes -- deno run -A --lock-write main.tsx" + "dev": "deno run -A lib/watch.ts components docs lib plugins routes -- deno run -A main.tsx" }, "lint": { "exclude": ["docs/esm"], @@ -17,8 +17,8 @@ "jsxImportSource": "revolution" }, "imports": { - "effection": "https://deno.land/x/effection@3.0.0-beta.2/mod.ts", - "revolution": "https://deno.land/x/revolution@0.5.2/mod.ts", - "revolution/jsx-runtime": "https://deno.land/x/revolution@0.5.2/jsx-runtime.ts" + "effection": "https://deno.land/x/effection@3.0.3/mod.ts", + "revolution": "https://deno.land/x/revolution@0.6.0/mod.ts", + "revolution/jsx-runtime": "https://deno.land/x/revolution@0.6.0/jsx-runtime.ts" } } diff --git a/www/deno.lock b/www/deno.lock index 87ddc3774..9b1807d93 100644 --- a/www/deno.lock +++ b/www/deno.lock @@ -2,6 +2,7 @@ "version": "3", "packages": { "specifiers": { + "jsr:@libs/xml": "jsr:@libs/xml@5.4.13", "jsr:@std/assert@^0.219.1": "jsr:@std/assert@0.219.1", "jsr:@std/async@^0.219.1": "jsr:@std/async@0.219.1", "jsr:@std/cli@^0.219.1": "jsr:@std/cli@0.219.1", @@ -28,6 +29,9 @@ "npm:unified@10.1.2": "npm:unified@10.1.2" }, "jsr": { + "@libs/xml@5.4.13": { + "integrity": "995320d1ce4a29ced82233e5e46d47a880e338197bbd257a686bf9afcc3ac0e4" + }, "@std/assert@0.219.1": { "integrity": "e76c2a1799a78f0f4db7de04bdc9b908a7a4b821bb65eda0285885297d4fb8af" }, @@ -1577,6 +1581,8 @@ "https://deno.land/std@0.158.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c", "https://deno.land/std@0.158.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832", "https://deno.land/std@0.158.0/testing/asserts.ts": "8696c488bc98d8d175e74dc652a0ffbc7fca93858da01edc57ed33c1148345da", + "https://deno.land/std@0.188.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", + "https://deno.land/std@0.188.0/flags/mod.ts": "17f444ddbee43c5487568de0c6a076c7729cfe90d96d2ffcd2b8f8adadafb6e8", "https://deno.land/std@0.201.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", "https://deno.land/std@0.201.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", "https://deno.land/std@0.201.0/encoding/base32.ts": "c329447451560ec692b9eb4d1badb6437f1d419ddbb21c1f994b0fe0b6b66cc8", @@ -1812,6 +1818,7 @@ "https://deno.land/x/effection@3.0.3/lib/run/frame.ts": "132fdace9c00e6ad0e249d7faab1c33680336c5fa8e4a893f092ecec4e2df786", "https://deno.land/x/effection@3.0.3/lib/run/scope.ts": "a968455e313ba9aa097ee5c18b4db0d8e2397b90c78e413fa08396baead7b74a", "https://deno.land/x/effection@3.0.3/lib/run/task.ts": "7084b9cabdc338c776dc522ec8b677fb3ac41aa0c94e454d467731494cb68737", + "https://deno.land/x/effection@3.0.3/lib/run/types.ts": "010bea700f68fef99dd87ca5ca3cbbc90e026ac467889d8429d39cba0ee55fda", "https://deno.land/x/effection@3.0.3/lib/run/value.ts": "d57428b45dfeecc9df1e68dadf8697dbc33cd412e6ffcab9d0ba4368e8c1fbd6", "https://deno.land/x/effection@3.0.3/lib/shift-sync.ts": "74ecefa9cb2e145a3c52f363319f8d6296b804600852044b7d14bd53bc10b512", "https://deno.land/x/effection@3.0.3/lib/signal.ts": "6aba1f372419e1540bd29a9ff992ffd2500e035b2e455d2c11d856a052f698d1", @@ -1827,6 +1834,7 @@ "https://deno.land/x/esbuild_deno_loader@0.8.2/src/plugin_deno_loader.ts": "166356133ee63d80e5559a10c18e10b625da96e39a4518b8c7adfef718bb4e32", "https://deno.land/x/esbuild_deno_loader@0.8.2/src/plugin_deno_resolver.ts": "0449ed23ae93db1ec74d015a46934aefd7ba7a8f719f7a4980b616cb3f5bbee4", "https://deno.land/x/esbuild_deno_loader@0.8.2/src/shared.ts": "33052684aeb542ebd24da372816bbbf885cd090a7ab0fde7770801f7f5b49572", + "https://deno.land/x/hastx@v0.0.10/deps.ts": "6c9b4e0a1d0120f3be92d74baa68b83b3730de22e7c4fdee7b2631189a8b3336", "https://deno.land/x/hastx@v0.0.10/html.ts": "54f86c5378dd7282142c9847efaf20b7ee244743f16af20a62900e912d1ae810", "https://deno.land/x/importmap@0.2.1/_util.ts": "ada9a9618b537e6c0316c048a898352396c882b9f2de38aba18fd3f2950ede89", "https://deno.land/x/importmap@0.2.1/mod.ts": "ae3d1cd7eabd18c01a4960d57db471126b020f23b37ef14e1359bbb949227ade", @@ -1851,6 +1859,27 @@ "https://deno.land/x/revolution@0.5.2/lib/server.ts": "560765955e3d6129f8f5d9fcaf4f598ddb0b9543db766f7a1a8d18d360b0a07c", "https://deno.land/x/revolution@0.5.2/lib/sse.ts": "df0a18d90ab20e4c707c59240a942c347bc7aa22c48601cb3a49ca12458a6e30", "https://deno.land/x/revolution@0.5.2/lib/types.ts": "2f32cbc673d496b5a37e5b9ff829577866d66a19b993ed59840b92700861d237", - "https://deno.land/x/revolution@0.5.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345" + "https://deno.land/x/revolution@0.5.2/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345", + "https://deno.land/x/revolution@0.6.0/jsx-runtime.ts": "b0d18239c9202c8881750902a8d19b07d84361ca4ab178edb1fd46b3542fd9a6", + "https://deno.land/x/revolution@0.6.0/lib/assertions.ts": "a3e142cc30ad9530fa97edecbc94272eefdd9398d927ba18b9e79684d6335889", + "https://deno.land/x/revolution@0.6.0/lib/builder.ts": "6d96073ef323ed5db796eaf5065b8f1a298552e9174c3da3d96a5e124c5b94a7", + "https://deno.land/x/revolution@0.6.0/lib/deps/effection.ts": "036e0ed764abc7550f2138391eb7119644f3cbc19433196459b126c85f5ffad7", + "https://deno.land/x/revolution@0.6.0/lib/deps/hast.ts": "c18e53e92fadfc87f4e27d7568a3d764e8059d38ce54a4f02dc75451c0f716e6", + "https://deno.land/x/revolution@0.6.0/lib/deps/std.ts": "b4e46fe71aef45c02e10825ac60aa8bf36dfb38d054dfb10f9b2d1bed8be9aef", + "https://deno.land/x/revolution@0.6.0/lib/island.ts": "7bfebfdc877648aadead95db7acab1ce249f43fb512bf11d6bb78727350012da", + "https://deno.land/x/revolution@0.6.0/lib/middleware.ts": "7e48ef017613fdd55488e4334b6027a4c41ec2a020c46df8090899f9497a32cb", + "https://deno.land/x/revolution@0.6.0/lib/middleware/concat.ts": "e875397258a40e8a948251876ab03945df2bb2ff67d8974a6fa299772a5ee5f5", + "https://deno.land/x/revolution@0.6.0/lib/middleware/dispatch.ts": "1af530a1c479fde6cc4d31f423f2fc03fc2b81f30bbb19800c3c0f478ef2a40a", + "https://deno.land/x/revolution@0.6.0/lib/middleware/http-responses.ts": "cce163033268305d3176a077176bba3a2df5da11c491c297cf20b8cc4dcb6bac", + "https://deno.land/x/revolution@0.6.0/lib/middleware/island-middleware.ts": "91c482145220521adb5d39a2b68a8c5fb3c695d7bb8b02de8bb1b7635743d5a3", + "https://deno.land/x/revolution@0.6.0/lib/middleware/serialize-html.ts": "dc4e6facc5db369b6f1a30a89901ea4fdce3715d625e226c5017f45e38d780e4", + "https://deno.land/x/revolution@0.6.0/lib/middleware/serve-dir.ts": "d9fd3f00ee153bac537e00fcad6df810ecf4bba645cf87924bd4780b6923c426", + "https://deno.land/x/revolution@0.6.0/lib/mod.ts": "0466ad5b79d6a76499f59c7f4ef941797fba496592cd5db954a3ba68b0b9362f", + "https://deno.land/x/revolution@0.6.0/lib/revolution.ts": "95ea50024cc35b65133e64c353c7aef15c792fbb033839c1b9bf7725bbb6cf8d", + "https://deno.land/x/revolution@0.6.0/lib/route.ts": "2043d2d7c857015d0c998aa7bfffd6b00fbc47f8adf7305abf9b691ff45a0a91", + "https://deno.land/x/revolution@0.6.0/lib/server.ts": "560765955e3d6129f8f5d9fcaf4f598ddb0b9543db766f7a1a8d18d360b0a07c", + "https://deno.land/x/revolution@0.6.0/lib/sse.ts": "3ace8fd4b2935e264940511366513bfeb5b414e457bc3d6a916d123b20a80d3c", + "https://deno.land/x/revolution@0.6.0/lib/types.ts": "2f32cbc673d496b5a37e5b9ff829577866d66a19b993ed59840b92700861d237", + "https://deno.land/x/revolution@0.6.0/mod.ts": "ffae461c16d4a1bf24c2179582ab8d5c81ad0df61e4ae2fba51ef5e5bdf90345" } } diff --git a/www/docs/docs.ts b/www/docs/docs.ts index 078eabfd5..001aa00ea 100644 --- a/www/docs/docs.ts +++ b/www/docs/docs.ts @@ -1,5 +1,12 @@ -import { call, type Operation, resource, type Task, useScope } from "effection"; -import structure from "./structure.json" assert { type: "json" }; +import { + all, + call, + type Operation, + resource, + type Task, + useScope, +} from "effection"; +import structure from "./structure.json" with { type: "json" }; import { basename } from "https://deno.land/std@0.205.0/path/posix/basename.ts"; @@ -19,6 +26,7 @@ export interface DocModule { } export interface Docs { + all(): Operation; getDoc(id?: string): Operation; } @@ -102,6 +110,12 @@ export function loadDocs(): Operation { } yield* provide({ + *all() { + if (!loaders) { + loaders = yield* load(); + } + return yield* all([...loaders.values()]); + }, *getDoc(id) { if (id) { if (!loaders) { diff --git a/www/lib/watch.ts b/www/lib/watch.ts index 354aa4426..ad27b02f6 100644 --- a/www/lib/watch.ts +++ b/www/lib/watch.ts @@ -1,10 +1,7 @@ import { - action, - filter, - first, - pipe, + call, + main, resource, - run, sleep, type Stream, stream, @@ -15,7 +12,7 @@ import { parse } from "https://deno.land/std@0.188.0/flags/mod.ts"; import { useCommand } from "./use-command.ts"; -await run(function* () { +await main(function* () { let scriptargs = parse(Deno.args, { "--": true, }); @@ -28,49 +25,36 @@ await run(function* () { console.log(`watch: ${JSON.stringify(paths)}`); console.log(`run: ${cmd} ${args.join(" ")}`); - yield* action(function* (resolve) { - Deno.addSignalListener("SIGINT", resolve); - try { - while (true) { - yield* action(function* (restart) { - let change = pipe( - useFsWatch(paths), - filter(function* (event) { - return !event.paths.some((path) => - ignores.some((ignore) => path.includes(ignore)) - ); - }), - ); - - yield* useCommand(cmd, { args }); - - yield* first(change); + while (true) { + yield* call(function* () { + let changes = yield* useFsWatch(paths); - yield* sleep(100); + yield* useCommand(cmd, { args }); - console.log("changes detected, restarting..."); - - restart(); - }); + while (true) { + let { value: change } = yield* changes.next(); + if ( + !change.paths.some((path) => + ignores.some((ignore) => path.includes(ignore)) + ) + ) { + break; + } } - } finally { - Deno.removeSignalListener("SIGINT", resolve); - } - }); + yield* sleep(100); + console.log("changes detected, restarting..."); + }); + } }); function useFsWatch(paths: string | string[]): Stream { - return { - subscribe() { - return resource(function* (provide) { - let watcher = Deno.watchFs(paths); - try { - let subscription = yield* stream(watcher).subscribe(); - yield* provide(subscription as Subscription); - } finally { - watcher.close(); - } - }); - }, - }; + return resource(function* (provide) { + let watcher = Deno.watchFs(paths); + try { + let subscription = yield* stream(watcher); + yield* provide(subscription as Subscription); + } finally { + watcher.close(); + } + }); } diff --git a/www/main.tsx b/www/main.tsx index e3fad3f51..727da08a0 100644 --- a/www/main.tsx +++ b/www/main.tsx @@ -1,6 +1,6 @@ import { main, suspend } from "effection"; -import { createRevolution, route } from "revolution"; +import { createRevolution } from "revolution"; import { docsRoute } from "./routes/docs-route.tsx"; import { indexRoute } from "./routes/index-route.tsx"; import { v2docsRoute } from "./routes/v2docs-route.tsx"; @@ -11,6 +11,7 @@ import { config } from "./tailwind.config.ts"; import { twindPlugin } from "./plugins/twind.ts"; import { rebasePlugin } from "./plugins/rebase.ts"; import { etagPlugin } from "./plugins/etag.ts"; +import { route, sitemapPlugin } from "./plugins/sitemap.ts"; import { loadDocs } from "./docs/docs.ts"; import { loadV2Docs } from "./docs/v2-docs.ts"; @@ -34,6 +35,7 @@ await main(function* () { twindPlugin({ config }), etagPlugin(), rebasePlugin(), + sitemapPlugin(), ], }); diff --git a/www/plugins/rebase.ts b/www/plugins/rebase.ts index a06ebb43f..1f2d1c385 100644 --- a/www/plugins/rebase.ts +++ b/www/plugins/rebase.ts @@ -61,14 +61,22 @@ export function rebasePlugin(): RevolutionPlugin { * with protocol. */ export function* useAbsoluteUrl(path: string): Operation { - let normalizedPath = posixNormalize(path); - if (normalizedPath.startsWith("/")) { - let base = yield* BaseUrl; - let url = new URL(base); - url.pathname = posixNormalize(`${base.pathname}${path}`); - return url.toString(); - } else { - let request = yield* CurrentRequest; - return new URL(path, request.url).toString(); + let absolute = yield* useAbsoluteUrlFactory(); + return absolute(path); +} + +export function* useAbsoluteUrlFactory(): Operation<(path: string) => string> { + let base = yield* BaseUrl; + let request = yield* CurrentRequest; + + return (path) => { + let normalizedPath = posixNormalize(path); + if (normalizedPath.startsWith("/")) { + let url = new URL(base); + url.pathname = posixNormalize(`${base.pathname}${path}`); + return url.toString(); + } else { + return new URL(path, request.url).toString(); + } } } diff --git a/www/plugins/sitemap.ts b/www/plugins/sitemap.ts new file mode 100644 index 000000000..7516e1693 --- /dev/null +++ b/www/plugins/sitemap.ts @@ -0,0 +1,125 @@ +import type { Middleware, RevolutionPlugin } from "revolution"; +import { useRevolutionOptions, route as revolutionRoute } from "revolution"; +import type { Operation } from "effection"; +import { stringify } from "jsr:@libs/xml"; +import { compile } from "https://deno.land/x/path_to_regexp@v6.2.1/index.ts"; +import { useAbsoluteUrlFactory } from "./rebase.ts"; + +export function sitemapPlugin(): RevolutionPlugin { + return { + *http(request, next) { + let options = yield* useRevolutionOptions(); + let url = new URL(request.url); + + if (url.pathname === "/sitemap.xml") { + let app = options.app ?? []; + let paths: RoutePath[] = []; + for (let middleware of app) { + let ext = middleware as SitemapExtension; + if (ext.sitemapExtension) { + paths = paths.concat(yield* ext.sitemapExtension(request)); + } + } + + let absolute = yield* useAbsoluteUrlFactory(); + + let xml = stringify({ + "@version": "1.0", + "@encoding": "UTF-8", + urlset: { + "@xmlns": "http://www.sitemaps.org/schemas/sitemap/0.9", + url: paths.map((path) => { + let { pathname, ...entry } = path; + + return { + loc: absolute(pathname), + ...entry, + }; + }), + }, + }); + + return new Response(xml, { + status: 200, + headers: { + "Content-Type": "application/xml", + }, + }); + } + return yield* next(request); + }, + }; +} + +export interface SitemapExtension { + sitemapExtension?(request: Request): Operation; +} + +export interface RoutePath { + pathname: string; + lastmod?: string; + changefreq?: + | "always" + | "hourly" + | "daily" + | "weekly" + | "monthly" + | "yearly" + | "never"; + priority?: number; +} + +/** + * Just like a route, but generates a sitemap for all the urls + */ +export interface SitemapRoute { + /** + * The HTTP or HTML handler for this route + */ + handler: Middleware; + + /** + * Generate a list of paths for this route. It will be passed a function which + * will substitute in the parameters of the route to generate the path as a string. + * For example: + * + * ```ts + * // assuming a route pattern: "/users/:username" + * generate({ username: 'cowboyd' }) //=> "/users/cowboyd" + * ``` + * @param generate - a function to generate a single pathname + * @param request - the request for the sitemap + * @returns a list of `RoutePath` values + */ + routemap?( + generate: (params?: Record) => string, + request: Request, + ): Operation; +} + +export function route( + pattern: string, + middleware: Middleware | SitemapRoute, +): Middleware { + if (isSitemapRoute(middleware)) { + let handler = revolutionRoute(pattern, middleware.handler); + if (middleware.routemap) { + const { routemap } = middleware; + Object.defineProperty(handler, "sitemapExtension", { + value(request: Request) { + let generate = compile(pattern); + return routemap(generate, request); + }, + }); + } + return handler; + } else { + return revolutionRoute(pattern, middleware); + } +} + +function isSitemapRoute( + o: Middleware | SitemapRoute, +): o is SitemapRoute { + return !!(o as SitemapRoute).handler; +} diff --git a/www/routes/app.html.tsx b/www/routes/app.html.tsx index 31625bb4e..36e6a53cd 100644 --- a/www/routes/app.html.tsx +++ b/www/routes/app.html.tsx @@ -5,7 +5,6 @@ import { useAbsoluteUrl } from "../plugins/rebase.ts"; import { Header } from "../components/header.tsx"; import { Footer } from "../components/footer.tsx"; - export interface Options { title: string; } @@ -20,7 +19,7 @@ export function* useAppHtml({ }: Options): Operation<({ children, navLinks }: AppHtmlProps) => JSX.Element> { let homeURL = yield* useAbsoluteUrl("/"); let twitterImageURL = yield* useAbsoluteUrl( - "/assets/images/meta-effection.png" + "/assets/images/meta-effection.png", ); return ({ children, navLinks }) => ( @@ -68,7 +67,9 @@ export function* useAppHtml({
-
{children}
+
+ {children} +