polen 0.11.0-next.27 → 0.11.0-next.29
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/build/api/builder/ssg/generate.d.ts.map +1 -1
- package/build/api/builder/ssg/generate.js +77 -50
- package/build/api/builder/ssg/generate.js.map +1 -1
- package/build/api/builder/ssg/page-generator.worker.js +63 -56
- package/build/api/builder/ssg/page-generator.worker.js.map +1 -1
- package/build/api/builder/ssg/server-runner.worker.js +34 -40
- package/build/api/builder/ssg/server-runner.worker.js.map +1 -1
- package/build/api/builder/ssg/worker-messages.d.ts +11 -0
- package/build/api/builder/ssg/worker-messages.d.ts.map +1 -1
- package/build/api/builder/ssg/worker-messages.js +8 -1
- package/build/api/builder/ssg/worker-messages.js.map +1 -1
- package/build/vite/plugins/main.js +1 -1
- package/build/vite/plugins/main.js.map +1 -1
- package/build/vite/plugins/routes-manifest.d.ts.map +1 -1
- package/build/vite/plugins/routes-manifest.js +5 -2
- package/build/vite/plugins/routes-manifest.js.map +1 -1
- package/package.json +37 -42
- package/src/api/builder/ssg/generate.ts +112 -71
- package/src/api/builder/ssg/page-generator.worker.ts +111 -75
- package/src/api/builder/ssg/server-runner.worker.ts +39 -44
- package/src/api/builder/ssg/worker-messages.ts +12 -1
- package/src/vite/plugins/main.ts +1 -1
- package/src/vite/plugins/routes-manifest.ts +5 -2
@@ -2,15 +2,11 @@
|
|
2
2
|
* Worker that generates static pages by fetching from servers.
|
3
3
|
* This is executed in a worker thread using Effect Worker API.
|
4
4
|
*/
|
5
|
-
import {
|
6
|
-
import { Path, WorkerRunner } from '@effect/platform'
|
5
|
+
import { FileSystem, Path, WorkerRunner } from '@effect/platform'
|
7
6
|
import { NodeContext, NodeRuntime, NodeWorkerRunner } from '@effect/platform-node'
|
8
|
-
import { Data, Effect, Layer } from 'effect'
|
9
|
-
import { promises as fs } from 'node:fs'
|
7
|
+
import { Array, Data, Duration, Effect, Either, Layer } from 'effect'
|
10
8
|
import { type GenerateResult, PageMessage } from './worker-messages.js'
|
11
9
|
|
12
|
-
const debug = debugPolen.sub(`api:ssg:page-generator`)
|
13
|
-
|
14
10
|
// ============================================================================
|
15
11
|
// Error Types
|
16
12
|
// ============================================================================
|
@@ -31,85 +27,122 @@ class RouteProcessingError extends Data.Error<{
|
|
31
27
|
// Generate Pages Handler
|
32
28
|
// ============================================================================
|
33
29
|
|
30
|
+
// Fetch HTML from server using Effect patterns
|
31
|
+
const fetchPage = (url: string) =>
|
32
|
+
Effect.gen(function*() {
|
33
|
+
const response = yield* Effect.tryPromise({
|
34
|
+
try: () => fetch(url),
|
35
|
+
catch: (error) => new Error(`Network error: ${error}`),
|
36
|
+
})
|
37
|
+
|
38
|
+
if (!response.ok) {
|
39
|
+
return yield* Effect.fail(
|
40
|
+
new Error(`HTTP ${response.status}: ${response.statusText}`),
|
41
|
+
)
|
42
|
+
}
|
43
|
+
|
44
|
+
return yield* Effect.tryPromise({
|
45
|
+
try: () => response.text(),
|
46
|
+
catch: (error) => new Error(`Failed to read response: ${error}`),
|
47
|
+
})
|
48
|
+
})
|
49
|
+
|
50
|
+
// Write HTML to file system
|
51
|
+
const writeHtmlFile = (outputPath: string, html: string) =>
|
52
|
+
Effect.gen(function*() {
|
53
|
+
const fs = yield* FileSystem.FileSystem
|
54
|
+
const path = yield* Path.Path
|
55
|
+
|
56
|
+
const dir = path.dirname(outputPath)
|
57
|
+
yield* fs.makeDirectory(dir, { recursive: true })
|
58
|
+
yield* fs.writeFileString(outputPath, html)
|
59
|
+
})
|
60
|
+
|
61
|
+
// Process a single route
|
62
|
+
const processRoute = (
|
63
|
+
route: string,
|
64
|
+
serverPort: number,
|
65
|
+
outputDir: string,
|
66
|
+
basePath?: string,
|
67
|
+
) =>
|
68
|
+
Effect.gen(function*() {
|
69
|
+
const path = yield* Path.Path
|
70
|
+
|
71
|
+
// Fetch the page from the server
|
72
|
+
// Construct URL with base path if provided
|
73
|
+
const basePathNormalized = basePath ? basePath.replace(/\/$/, '') : ''
|
74
|
+
const url = `http://localhost:${serverPort}${basePathNormalized}${route}`
|
75
|
+
const html = yield* fetchPage(url).pipe(
|
76
|
+
Effect.mapError(error => new RouteProcessingError({ route, cause: error })),
|
77
|
+
)
|
78
|
+
|
79
|
+
// Determine output file path
|
80
|
+
const outputPath = path.join(
|
81
|
+
outputDir,
|
82
|
+
route === '/' ? 'index.html' : `${route.slice(1)}/index.html`,
|
83
|
+
)
|
84
|
+
|
85
|
+
// Write the HTML file
|
86
|
+
yield* writeHtmlFile(outputPath, html).pipe(
|
87
|
+
Effect.mapError(error => new RouteProcessingError({ route, cause: error })),
|
88
|
+
)
|
89
|
+
|
90
|
+
return route
|
91
|
+
})
|
92
|
+
|
34
93
|
const handlers = {
|
35
94
|
GeneratePages: (
|
36
|
-
{ routes, serverPort, outputDir }: { routes: readonly string[]; serverPort: number; outputDir: string },
|
95
|
+
{ routes, serverPort, outputDir, basePath }: { routes: readonly string[]; serverPort: number; outputDir: string; basePath?: string | undefined },
|
37
96
|
) =>
|
38
97
|
Effect.gen(function*() {
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
)
|
67
|
-
|
68
|
-
// Ensure directory exists
|
69
|
-
const dir = path.dirname(outputPath)
|
70
|
-
await fs.mkdir(dir, { recursive: true })
|
71
|
-
|
72
|
-
// Write the HTML file
|
73
|
-
await fs.writeFile(outputPath, html, 'utf-8')
|
74
|
-
|
75
|
-
processedCount++
|
76
|
-
|
77
|
-
// Log progress every 5 routes or on last route
|
78
|
-
if (processedCount % 5 === 0 || processedCount === routes.length) {
|
79
|
-
debug(`Progress`, {
|
80
|
-
processedCount,
|
81
|
-
totalRoutes: routes.length,
|
82
|
-
serverPort,
|
83
|
-
})
|
84
|
-
}
|
85
|
-
},
|
86
|
-
catch: (error) => {
|
87
|
-
debug(`Failed to process route`, { route, error })
|
88
|
-
return new RouteProcessingError({ route, cause: error })
|
89
|
-
},
|
90
|
-
})
|
91
|
-
}
|
98
|
+
yield* Effect.logDebug(`Starting batch generation`).pipe(
|
99
|
+
Effect.annotateLogs({
|
100
|
+
totalRoutes: routes.length,
|
101
|
+
serverPort,
|
102
|
+
basePath: basePath || 'none',
|
103
|
+
}),
|
104
|
+
)
|
105
|
+
|
106
|
+
// Process all routes with timing, collecting both successes and failures
|
107
|
+
const [duration, results] = yield* Effect.forEach(
|
108
|
+
routes,
|
109
|
+
(route, index) =>
|
110
|
+
processRoute(route, serverPort, outputDir, basePath).pipe(
|
111
|
+
Effect.tap(() =>
|
112
|
+
// Log progress every 5 routes
|
113
|
+
(index + 1) % 5 === 0 || index === routes.length - 1
|
114
|
+
? Effect.logDebug(`Progress: ${index + 1}/${routes.length} routes processed`)
|
115
|
+
: Effect.void
|
116
|
+
),
|
117
|
+
Effect.either, // Convert to Either to capture both success and failure
|
118
|
+
),
|
119
|
+
{ concurrency: 1 }, // Process sequentially to avoid overwhelming the server
|
120
|
+
).pipe(Effect.timed)
|
121
|
+
|
122
|
+
// Partition results into successes and failures
|
123
|
+
const [failures, successes] = Array.partition(results, Either.isRight)
|
124
|
+
const processedCount = successes.length
|
92
125
|
|
93
126
|
const result: GenerateResult = {
|
94
|
-
success:
|
127
|
+
success: failures.length === 0,
|
95
128
|
processedCount,
|
96
|
-
duration:
|
129
|
+
duration: Duration.toMillis(duration),
|
97
130
|
memoryUsed: process.memoryUsage().heapUsed,
|
131
|
+
...(failures.length > 0 && {
|
132
|
+
error: `Failed to generate ${failures.length} out of ${routes.length} routes`,
|
133
|
+
failures: failures.map((f) => ({
|
134
|
+
route: f.left.route,
|
135
|
+
error: f.left.cause instanceof Error ? f.left.cause.message : String(f.left.cause),
|
136
|
+
})),
|
137
|
+
}),
|
98
138
|
}
|
99
139
|
|
100
|
-
|
140
|
+
yield* Effect.logDebug(`Batch generation complete`).pipe(
|
141
|
+
Effect.annotateLogs(result),
|
142
|
+
)
|
143
|
+
|
101
144
|
return result
|
102
|
-
})
|
103
|
-
Effect.catchAll((error) =>
|
104
|
-
Effect.succeed({
|
105
|
-
success: false,
|
106
|
-
processedCount: 0,
|
107
|
-
duration: Date.now() - Date.now(),
|
108
|
-
memoryUsed: process.memoryUsage().heapUsed,
|
109
|
-
error: error instanceof Error ? error.message : String(error),
|
110
|
-
})
|
111
|
-
),
|
112
|
-
),
|
145
|
+
}),
|
113
146
|
}
|
114
147
|
|
115
148
|
// ============================================================================
|
@@ -120,6 +153,9 @@ const handlers = {
|
|
120
153
|
WorkerRunner.launch(
|
121
154
|
Layer.provide(
|
122
155
|
WorkerRunner.layerSerialized(PageMessage, handlers),
|
123
|
-
Layer.
|
156
|
+
Layer.mergeAll(
|
157
|
+
NodeWorkerRunner.layer,
|
158
|
+
NodeContext.layer,
|
159
|
+
),
|
124
160
|
),
|
125
161
|
).pipe(NodeRuntime.runMain)
|
@@ -2,18 +2,15 @@
|
|
2
2
|
* Worker that runs a Polen server for SSG.
|
3
3
|
* This is executed in a child process using Effect Worker API.
|
4
4
|
*/
|
5
|
-
import { debugPolen } from '#singletons/debug'
|
6
5
|
import { WorkerRunner } from '@effect/platform'
|
7
6
|
import { NodeRuntime, NodeWorkerRunner } from '@effect/platform-node'
|
8
|
-
import { Duration, Effect, Layer, Scope } from 'effect'
|
7
|
+
import { Duration, Effect, Layer, Ref, Scope } from 'effect'
|
9
8
|
import { spawn } from 'node:child_process'
|
10
9
|
import type { ChildProcess } from 'node:child_process'
|
11
10
|
import { ServerMessage } from './worker-messages.js'
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
// Store the server process for cleanup
|
16
|
-
let serverProcess: ChildProcess | null = null
|
12
|
+
// Store the server process reference for cleanup
|
13
|
+
const serverProcessRef = Ref.unsafeMake<ChildProcess | null>(null)
|
17
14
|
|
18
15
|
// ============================================================================
|
19
16
|
// Handlers
|
@@ -23,20 +20,21 @@ const handlers = {
|
|
23
20
|
StartServer: ({ serverPath, port }: { serverPath: string; port: number }) =>
|
24
21
|
Effect.gen(function*() {
|
25
22
|
// If there's already a server running, stop it first
|
26
|
-
|
27
|
-
|
23
|
+
const existingProcess = yield* Ref.get(serverProcessRef)
|
24
|
+
if (existingProcess) {
|
25
|
+
existingProcess.kill('SIGTERM')
|
28
26
|
yield* Effect.sleep(Duration.millis(500))
|
29
|
-
if (!
|
30
|
-
|
27
|
+
if (!existingProcess.killed) {
|
28
|
+
existingProcess.kill('SIGKILL')
|
31
29
|
}
|
32
|
-
|
30
|
+
yield* Ref.set(serverProcessRef, null)
|
33
31
|
}
|
34
32
|
|
35
33
|
// Start the server process
|
36
|
-
|
37
|
-
debug(`Starting server with command: node ${serverPath}`)
|
34
|
+
yield* Effect.logDebug(`Starting server with command: node ${serverPath}`)
|
38
35
|
|
39
|
-
|
36
|
+
const proc = yield* Effect.sync(() => {
|
37
|
+
const serverProc = spawn('node', [serverPath], {
|
40
38
|
env: {
|
41
39
|
...process.env,
|
42
40
|
PORT: port.toString(),
|
@@ -44,18 +42,23 @@ const handlers = {
|
|
44
42
|
stdio: ['ignore', 'pipe', 'pipe'],
|
45
43
|
})
|
46
44
|
|
47
|
-
|
48
|
-
|
45
|
+
// Log server output
|
46
|
+
serverProc.stdout?.on('data', (data) => {
|
47
|
+
Effect.logDebug(`[Server ${port}] stdout: ${data.toString().trim()}`).pipe(
|
48
|
+
Effect.runSync,
|
49
|
+
)
|
49
50
|
})
|
50
51
|
|
51
|
-
|
52
|
-
|
52
|
+
serverProc.stderr?.on('data', (data) => {
|
53
|
+
Effect.logDebug(`[Server ${port}] stderr: ${data.toString().trim()}`).pipe(
|
54
|
+
Effect.runSync,
|
55
|
+
)
|
53
56
|
})
|
54
57
|
|
55
|
-
return
|
58
|
+
return serverProc
|
56
59
|
})
|
57
60
|
|
58
|
-
|
61
|
+
yield* Ref.set(serverProcessRef, proc)
|
59
62
|
|
60
63
|
// Wait for server to be ready with proper interruption support
|
61
64
|
const waitForReady = Effect.async<void>((resume) => {
|
@@ -80,7 +83,7 @@ const handlers = {
|
|
80
83
|
try {
|
81
84
|
const response = await fetch(`http://localhost:${port}/`)
|
82
85
|
if (response.ok || response.status === 404) {
|
83
|
-
|
86
|
+
Effect.logDebug(`[Server ${port}] Ready!`).pipe(Effect.runSync)
|
84
87
|
if (checkInterval) clearInterval(checkInterval)
|
85
88
|
proc.removeListener('error', errorHandler)
|
86
89
|
proc.removeListener('exit', exitHandler)
|
@@ -114,33 +117,25 @@ const handlers = {
|
|
114
117
|
Effect.die(new Error(`Server on port ${port} failed to start within 30 seconds`))),
|
115
118
|
)
|
116
119
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
debug(`Finalizer: Stopping server on port ${port}`)
|
121
|
-
if (serverProcess && !serverProcess.killed) {
|
122
|
-
serverProcess.kill('SIGTERM')
|
123
|
-
// Try to kill forcefully after a brief wait
|
124
|
-
setTimeout(() => {
|
125
|
-
if (serverProcess && !serverProcess.killed) {
|
126
|
-
serverProcess.kill('SIGKILL')
|
127
|
-
}
|
128
|
-
serverProcess = null
|
129
|
-
}, 100)
|
130
|
-
}
|
131
|
-
})
|
132
|
-
)
|
133
|
-
}).pipe(Effect.scoped),
|
134
|
-
StopServer: () =>
|
120
|
+
yield* Effect.logDebug(`Server on port ${port} started successfully`)
|
121
|
+
}),
|
122
|
+
StopServer: ({ port }: { port?: number | undefined }) =>
|
135
123
|
Effect.gen(function*() {
|
136
|
-
|
137
|
-
|
124
|
+
const serverProc = yield* Ref.get(serverProcessRef)
|
125
|
+
if (serverProc) {
|
126
|
+
if (port !== undefined) {
|
127
|
+
yield* Effect.logDebug(`Stopping server on port ${port}`)
|
128
|
+
}
|
129
|
+
serverProc.kill('SIGTERM')
|
138
130
|
// Give it time to shut down gracefully
|
139
131
|
yield* Effect.sleep(Duration.millis(500))
|
140
|
-
if (!
|
141
|
-
|
132
|
+
if (!serverProc.killed) {
|
133
|
+
serverProc.kill('SIGKILL')
|
134
|
+
}
|
135
|
+
yield* Ref.set(serverProcessRef, null)
|
136
|
+
if (port !== undefined) {
|
137
|
+
yield* Effect.logDebug(`Server on port ${port} stopped`)
|
142
138
|
}
|
143
|
-
serverProcess = null
|
144
139
|
}
|
145
140
|
}),
|
146
141
|
}
|
@@ -16,6 +16,14 @@ const GenerateResultSchema = S.Struct({
|
|
16
16
|
duration: S.Number,
|
17
17
|
memoryUsed: S.Number,
|
18
18
|
error: S.optional(S.String),
|
19
|
+
failures: S.optional(
|
20
|
+
S.Array(
|
21
|
+
S.Struct({
|
22
|
+
route: S.String,
|
23
|
+
error: S.String,
|
24
|
+
}),
|
25
|
+
),
|
26
|
+
),
|
19
27
|
})
|
20
28
|
|
21
29
|
export type GenerateResult = S.Schema.Type<typeof GenerateResultSchema>
|
@@ -40,7 +48,9 @@ export class StopServerMessage extends S.TaggedRequest<StopServerMessage>()(
|
|
40
48
|
'StopServer',
|
41
49
|
{
|
42
50
|
failure: S.Never,
|
43
|
-
payload: {
|
51
|
+
payload: {
|
52
|
+
port: S.optional(S.Number),
|
53
|
+
},
|
44
54
|
success: S.Void,
|
45
55
|
},
|
46
56
|
) {}
|
@@ -60,6 +70,7 @@ export class GeneratePagesMessage extends S.TaggedRequest<GeneratePagesMessage>(
|
|
60
70
|
routes: S.Array(RouteSchema),
|
61
71
|
serverPort: S.Number,
|
62
72
|
outputDir: S.String,
|
73
|
+
basePath: S.optional(S.String),
|
63
74
|
},
|
64
75
|
success: GenerateResultSchema,
|
65
76
|
},
|
package/src/vite/plugins/main.ts
CHANGED
@@ -2,7 +2,7 @@ import type { Api } from '#api/$'
|
|
2
2
|
import type { Vite } from '#dep/vite/index'
|
3
3
|
import { Manifest } from '#vite/plugins/manifest'
|
4
4
|
import { vitePluginSsrCss } from '@hiogawa/vite-plugin-ssr-css'
|
5
|
-
import ViteReact from '@vitejs/plugin-react
|
5
|
+
import ViteReact from '@vitejs/plugin-react'
|
6
6
|
import { Path } from '@wollybeard/kit'
|
7
7
|
import Inspect from 'vite-plugin-inspect'
|
8
8
|
import { Branding } from './branding.js'
|
@@ -3,6 +3,7 @@ import { Vite } from '#dep/vite/index'
|
|
3
3
|
import { Catalog } from '#lib/catalog/$'
|
4
4
|
import { FileRouter } from '#lib/file-router/$'
|
5
5
|
import { SchemaDefinition } from '#lib/schema-definition/$'
|
6
|
+
import { Version } from '#lib/version/$'
|
6
7
|
import { debugPolen } from '#singletons/debug'
|
7
8
|
import * as NodeFileSystem from '@effect/platform-node/NodeFileSystem'
|
8
9
|
import consola from 'consola'
|
@@ -102,12 +103,14 @@ function processVersionedCatalog(
|
|
102
103
|
): void {
|
103
104
|
for (const schema of Catalog.Versioned.getAll(catalog)) {
|
104
105
|
const version = schema.version
|
105
|
-
|
106
|
+
// Properly encode the version to its string representation
|
107
|
+
const versionValue = Version.encodeSync(version)
|
108
|
+
routes.push(`/reference/version/${versionValue}`)
|
106
109
|
|
107
110
|
processSchemaDefinition(
|
108
111
|
schema.definition,
|
109
112
|
routes,
|
110
|
-
`/reference/version/${
|
113
|
+
`/reference/version/${versionValue}`,
|
111
114
|
)
|
112
115
|
}
|
113
116
|
}
|