@pyreon/server 0.11.3 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/server",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "SSR handler, SSG prerender, and island architecture for Pyreon",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,12 +44,12 @@
44
44
  "prepublishOnly": "bun run build"
45
45
  },
46
46
  "dependencies": {
47
- "@pyreon/core": "^0.11.3",
48
- "@pyreon/head": "^0.11.3",
49
- "@pyreon/reactivity": "^0.11.3",
50
- "@pyreon/router": "^0.11.3",
51
- "@pyreon/runtime-dom": "^0.11.3",
52
- "@pyreon/runtime-server": "^0.11.3"
47
+ "@pyreon/core": "^0.11.4",
48
+ "@pyreon/head": "^0.11.4",
49
+ "@pyreon/reactivity": "^0.11.4",
50
+ "@pyreon/router": "^0.11.4",
51
+ "@pyreon/runtime-dom": "^0.11.4",
52
+ "@pyreon/runtime-server": "^0.11.4"
53
53
  },
54
54
  "publishConfig": {
55
55
  "access": "public"
@@ -1,7 +1,15 @@
1
1
  import type { ComponentFn, VNode } from "@pyreon/core"
2
2
  import { h } from "@pyreon/core"
3
3
  import { createHandler } from "../handler"
4
- import { buildScripts, DEFAULT_TEMPLATE, processTemplate } from "../html"
4
+ import {
5
+ buildClientEntryTag,
6
+ buildScripts,
7
+ buildScriptsFast,
8
+ compileTemplate,
9
+ DEFAULT_TEMPLATE,
10
+ processCompiledTemplate,
11
+ processTemplate,
12
+ } from "../html"
5
13
  import { island } from "../island"
6
14
  import type { Middleware } from "../middleware"
7
15
  import { prerender } from "../ssg"
@@ -632,3 +640,160 @@ describe("prerender", () => {
632
640
  await rm(tmpDir, { recursive: true, force: true })
633
641
  })
634
642
  })
643
+
644
+ // ─── compileTemplate ─────────────────────────────────────────────────────────
645
+
646
+ describe("compileTemplate", () => {
647
+ test("splits template into 4 parts", () => {
648
+ const compiled = compileTemplate(DEFAULT_TEMPLATE)
649
+ expect(compiled.parts).toHaveLength(4)
650
+ })
651
+
652
+ test("throws when template is missing <!--pyreon-app-->", () => {
653
+ expect(() => compileTemplate("<html><!--pyreon-head--><!--pyreon-scripts--></html>")).toThrow(
654
+ "Template must contain <!--pyreon-app-->",
655
+ )
656
+ })
657
+
658
+ test("handles template with all three placeholders in custom layout", () => {
659
+ const tpl =
660
+ "<head><!--pyreon-head--></head><main><!--pyreon-app--></main><footer><!--pyreon-scripts--></footer>"
661
+ const compiled = compileTemplate(tpl)
662
+ const result = processCompiledTemplate(compiled, {
663
+ head: "<title>Hi</title>",
664
+ app: "<div>App</div>",
665
+ scripts: "<script></script>",
666
+ })
667
+ expect(result).toBe(
668
+ "<head><title>Hi</title></head><main><div>App</div></main><footer><script></script></footer>",
669
+ )
670
+ })
671
+
672
+ test("handles template without <!--pyreon-scripts--> placeholder", () => {
673
+ const tpl = "<html><!--pyreon-head--><body><!--pyreon-app--></body></html>"
674
+ const compiled = compileTemplate(tpl)
675
+ expect(compiled.parts[3]).toBe("") // after-scripts is empty
676
+ })
677
+ })
678
+
679
+ // ─── processCompiledTemplate ─────────────────────────────────────────────────
680
+
681
+ describe("processCompiledTemplate", () => {
682
+ test("produces same result as processTemplate", () => {
683
+ const data = {
684
+ head: "<title>Test</title>",
685
+ app: "<div>Hello</div>",
686
+ scripts: '<script type="module" src="/app.js"></script>',
687
+ }
688
+ const simple = processTemplate(DEFAULT_TEMPLATE, data)
689
+ const compiled = compileTemplate(DEFAULT_TEMPLATE)
690
+ const fast = processCompiledTemplate(compiled, data)
691
+ expect(fast).toBe(simple)
692
+ })
693
+
694
+ test("works with empty data", () => {
695
+ const compiled = compileTemplate(DEFAULT_TEMPLATE)
696
+ const result = processCompiledTemplate(compiled, { head: "", app: "", scripts: "" })
697
+ expect(result).not.toContain("<!--pyreon-head-->")
698
+ expect(result).not.toContain("<!--pyreon-app-->")
699
+ expect(result).not.toContain("<!--pyreon-scripts-->")
700
+ })
701
+ })
702
+
703
+ // ─── buildClientEntryTag ─────────────────────────────────────────────────────
704
+
705
+ describe("buildClientEntryTag", () => {
706
+ test("emits a module script tag with src", () => {
707
+ const tag = buildClientEntryTag("/dist/client.js")
708
+ expect(tag).toBe('<script type="module" src="/dist/client.js"></script>')
709
+ })
710
+ })
711
+
712
+ // ─── buildScriptsFast ────────────────────────────────────────────────────────
713
+
714
+ describe("buildScriptsFast", () => {
715
+ test("returns only client entry tag when no loader data", () => {
716
+ const tag = buildClientEntryTag("/app.js")
717
+ const result = buildScriptsFast(tag, null)
718
+ expect(result).toBe(tag)
719
+ })
720
+
721
+ test("returns only client entry tag when loader data is empty object", () => {
722
+ const tag = buildClientEntryTag("/app.js")
723
+ const result = buildScriptsFast(tag, {})
724
+ expect(result).toBe(tag)
725
+ })
726
+
727
+ test("includes inline loader data when present", () => {
728
+ const tag = buildClientEntryTag("/app.js")
729
+ const result = buildScriptsFast(tag, { users: [1, 2] })
730
+ expect(result).toContain("__PYREON_LOADER_DATA__")
731
+ expect(result).toContain('"users"')
732
+ expect(result).toContain(tag)
733
+ })
734
+
735
+ test("escapes </script> in loader data JSON", () => {
736
+ const tag = buildClientEntryTag("/app.js")
737
+ const result = buildScriptsFast(tag, { html: "</script>" })
738
+ expect(result).not.toContain("</script><")
739
+ expect(result).toContain("<\\/script>")
740
+ })
741
+ })
742
+
743
+ // ─── Middleware chaining edge cases ──────────────────────────────────────────
744
+
745
+ describe("middleware — edge cases", () => {
746
+ const App: ComponentFn = () => h("div", null, "app")
747
+ const routes = [{ path: "/", component: App }]
748
+
749
+ test("middleware can modify locals for downstream middleware", async () => {
750
+ const log: string[] = []
751
+ const mw1: Middleware = (ctx) => {
752
+ ctx.locals.user = "alice"
753
+ }
754
+ const mw2: Middleware = (ctx) => {
755
+ log.push(`user=${ctx.locals.user}`)
756
+ }
757
+ const handler = createHandler({ App, routes, middleware: [mw1, mw2] })
758
+ await handler(new Request("http://localhost/"))
759
+ expect(log).toEqual(["user=alice"])
760
+ })
761
+
762
+ test("early short-circuit prevents later middleware from running", async () => {
763
+ const log: number[] = []
764
+ const mw1: Middleware = () => {
765
+ log.push(1)
766
+ return new Response("blocked", { status: 403 })
767
+ }
768
+ const mw2: Middleware = () => {
769
+ log.push(2) // should never run
770
+ }
771
+ const handler = createHandler({ App, routes, middleware: [mw1, mw2] })
772
+ const res = await handler(new Request("http://localhost/"))
773
+ expect(res.status).toBe(403)
774
+ expect(log).toEqual([1]) // mw2 never ran
775
+ })
776
+
777
+ test("async middleware is supported", async () => {
778
+ const mw: Middleware = async (ctx) => {
779
+ await new Promise((r) => setTimeout(r, 1))
780
+ ctx.headers.set("X-Async", "true")
781
+ }
782
+ const handler = createHandler({ App, routes, middleware: [mw] })
783
+ const res = await handler(new Request("http://localhost/"))
784
+ expect(res.headers.get("X-Async")).toBe("true")
785
+ })
786
+
787
+ test("middleware receives parsed URL and path", async () => {
788
+ let receivedPath = ""
789
+ let receivedSearch = ""
790
+ const mw: Middleware = (ctx) => {
791
+ receivedPath = ctx.path
792
+ receivedSearch = ctx.url.search
793
+ }
794
+ const handler = createHandler({ App, routes, middleware: [mw] })
795
+ await handler(new Request("http://localhost/about?foo=bar"))
796
+ expect(receivedPath).toBe("/about?foo=bar")
797
+ expect(receivedSearch).toBe("?foo=bar")
798
+ })
799
+ })