@pyreon/server 0.11.2 → 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 +7 -7
- package/src/tests/server.test.ts +166 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/server",
|
|
3
|
-
"version": "0.11.
|
|
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.
|
|
48
|
-
"@pyreon/head": "^0.11.
|
|
49
|
-
"@pyreon/reactivity": "^0.11.
|
|
50
|
-
"@pyreon/router": "^0.11.
|
|
51
|
-
"@pyreon/runtime-dom": "^0.11.
|
|
52
|
-
"@pyreon/runtime-server": "^0.11.
|
|
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"
|
package/src/tests/server.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
})
|