@pikku/core 0.12.56 → 0.12.57
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/CHANGELOG.md +12 -0
- package/dist/dev/hot-reload.d.ts +7 -3
- package/dist/dev/hot-reload.js +90 -47
- package/dist/dev/reload-meta.d.ts +24 -0
- package/dist/dev/reload-meta.js +99 -0
- package/package.json +1 -1
- package/src/dev/hot-reload.test.ts +15 -8
- package/src/dev/hot-reload.ts +99 -54
- package/src/dev/reload-meta.test.ts +154 -0
- package/src/dev/reload-meta.ts +138 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, test, beforeEach, afterEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtemp, writeFile, rm, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
import { pikkuState, resetPikkuState } from '../pikku-state.js'
|
|
8
|
+
import { getSchema } from '../schema.js'
|
|
9
|
+
import { reloadGeneratedMeta } from './reload-meta.js'
|
|
10
|
+
|
|
11
|
+
const createMockLogger = () => {
|
|
12
|
+
const logs: Array<{ level: string; message: string }> = []
|
|
13
|
+
return {
|
|
14
|
+
info: (msg: string) => logs.push({ level: 'info', message: String(msg) }),
|
|
15
|
+
warn: (msg: string) => logs.push({ level: 'warn', message: String(msg) }),
|
|
16
|
+
error: (msg: string | Error) =>
|
|
17
|
+
logs.push({
|
|
18
|
+
level: 'error',
|
|
19
|
+
message: msg instanceof Error ? msg.message : String(msg),
|
|
20
|
+
}),
|
|
21
|
+
debug: (msg: string) => logs.push({ level: 'debug', message: String(msg) }),
|
|
22
|
+
getLogs: () => logs,
|
|
23
|
+
setLevel: () => {},
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const compiled: string[] = []
|
|
28
|
+
const mockSchemaService = {
|
|
29
|
+
compileSchema: (name: string, _value: any) => {
|
|
30
|
+
compiled.push(name)
|
|
31
|
+
},
|
|
32
|
+
validateSchema: () => {},
|
|
33
|
+
getSchemaNames: () => new Set(compiled),
|
|
34
|
+
getSchemaKeys: () => [],
|
|
35
|
+
} as any
|
|
36
|
+
|
|
37
|
+
describe('reloadGeneratedMeta', { concurrency: false }, () => {
|
|
38
|
+
let tmpDir: string
|
|
39
|
+
let mockLogger: ReturnType<typeof createMockLogger>
|
|
40
|
+
|
|
41
|
+
beforeEach(async () => {
|
|
42
|
+
resetPikkuState()
|
|
43
|
+
compiled.length = 0
|
|
44
|
+
tmpDir = await mkdtemp(join(tmpdir(), 'pikku-reload-meta-test-'))
|
|
45
|
+
mockLogger = createMockLogger()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
await rm(tmpDir, { recursive: true, force: true })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('reads generated meta json into pikku state', async () => {
|
|
53
|
+
await mkdir(join(tmpDir, 'function'), { recursive: true })
|
|
54
|
+
await mkdir(join(tmpDir, 'rpc'), { recursive: true })
|
|
55
|
+
await writeFile(
|
|
56
|
+
join(tmpDir, 'function/pikku-functions-meta.gen.json'),
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
newFunc: { pikkuFuncId: 'newFunc', inputSchemaName: 'NewFuncInput' },
|
|
59
|
+
})
|
|
60
|
+
)
|
|
61
|
+
await writeFile(
|
|
62
|
+
join(tmpDir, 'rpc/pikku-rpc-wirings-meta.internal.gen.json'),
|
|
63
|
+
JSON.stringify({ newFunc: 'newFunc' })
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
await reloadGeneratedMeta({
|
|
67
|
+
pikkuDir: tmpDir,
|
|
68
|
+
logger: mockLogger,
|
|
69
|
+
schemaService: mockSchemaService,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const functionsMeta = pikkuState(null, 'function', 'meta') as any
|
|
73
|
+
assert.equal(functionsMeta.newFunc.pikkuFuncId, 'newFunc')
|
|
74
|
+
const rpcMeta = pikkuState(null, 'rpc', 'meta') as any
|
|
75
|
+
assert.equal(rpcMeta.newFunc, 'newFunc')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('preserves runtime-registered internals (workflow orchestrator) across reload', async () => {
|
|
79
|
+
// The workflow service registers these at init; they are never in the
|
|
80
|
+
// generated JSON, so the reload must merge — not replace — the meta maps.
|
|
81
|
+
pikkuState(null, 'function', 'meta', {
|
|
82
|
+
pikkuWorkflowOrchestrator: { pikkuFuncId: 'pikkuWorkflowOrchestrator' },
|
|
83
|
+
})
|
|
84
|
+
pikkuState(null, 'queue', 'meta', {
|
|
85
|
+
'pikku-workflow-orchestrator': {
|
|
86
|
+
pikkuFuncId: 'pikkuWorkflowOrchestrator',
|
|
87
|
+
name: 'pikku-workflow-orchestrator',
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
await mkdir(join(tmpDir, 'function'), { recursive: true })
|
|
92
|
+
await mkdir(join(tmpDir, 'queue'), { recursive: true })
|
|
93
|
+
await writeFile(
|
|
94
|
+
join(tmpDir, 'function/pikku-functions-meta.gen.json'),
|
|
95
|
+
JSON.stringify({ userFunc: { pikkuFuncId: 'userFunc' } })
|
|
96
|
+
)
|
|
97
|
+
await writeFile(
|
|
98
|
+
join(tmpDir, 'queue/pikku-queue-workers-wirings-meta.gen.json'),
|
|
99
|
+
JSON.stringify({ userQueue: { pikkuFuncId: 'userFunc', name: 'userQueue' } })
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
await reloadGeneratedMeta({
|
|
103
|
+
pikkuDir: tmpDir,
|
|
104
|
+
logger: mockLogger,
|
|
105
|
+
schemaService: mockSchemaService,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const functionsMeta = pikkuState(null, 'function', 'meta') as any
|
|
109
|
+
assert.equal(
|
|
110
|
+
functionsMeta.pikkuWorkflowOrchestrator?.pikkuFuncId,
|
|
111
|
+
'pikkuWorkflowOrchestrator',
|
|
112
|
+
'runtime-registered internal must survive the reload'
|
|
113
|
+
)
|
|
114
|
+
assert.equal(functionsMeta.userFunc?.pikkuFuncId, 'userFunc')
|
|
115
|
+
const queueMeta = pikkuState(null, 'queue', 'meta') as any
|
|
116
|
+
assert.equal(
|
|
117
|
+
queueMeta['pikku-workflow-orchestrator']?.pikkuFuncId,
|
|
118
|
+
'pikkuWorkflowOrchestrator'
|
|
119
|
+
)
|
|
120
|
+
assert.equal(queueMeta.userQueue?.name, 'userQueue')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('re-adds generated json schemas and recompiles them', async () => {
|
|
124
|
+
const schemasDir = join(tmpDir, 'schemas', 'schemas')
|
|
125
|
+
await mkdir(schemasDir, { recursive: true })
|
|
126
|
+
await writeFile(
|
|
127
|
+
join(schemasDir, 'NewFuncInput.schema.json'),
|
|
128
|
+
JSON.stringify({ type: 'object', properties: { id: { type: 'string' } } })
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
await reloadGeneratedMeta({
|
|
132
|
+
pikkuDir: tmpDir,
|
|
133
|
+
logger: mockLogger,
|
|
134
|
+
schemaService: mockSchemaService,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const schema = getSchema('NewFuncInput') as any
|
|
138
|
+
assert.equal(schema.type, 'object')
|
|
139
|
+
assert.ok(
|
|
140
|
+
compiled.includes('NewFuncInput'),
|
|
141
|
+
'Schema should be recompiled after reload'
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('missing meta files and schemas dir are not an error', async () => {
|
|
146
|
+
await reloadGeneratedMeta({
|
|
147
|
+
pikkuDir: tmpDir,
|
|
148
|
+
logger: mockLogger,
|
|
149
|
+
schemaService: mockSchemaService,
|
|
150
|
+
})
|
|
151
|
+
const errors = mockLogger.getLogs().filter((l) => l.level === 'error')
|
|
152
|
+
assert.deepEqual(errors, [])
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { pikkuState } from '../pikku-state.js'
|
|
5
|
+
import { addSchema, compileAllSchemas } from '../schema.js'
|
|
6
|
+
import { clearMiddlewareCache } from '../middleware-runner.js'
|
|
7
|
+
import { clearPermissionsCache } from '../permissions.js'
|
|
8
|
+
import { clearChannelMiddlewareCache } from '../wirings/channel/channel-middleware-runner.js'
|
|
9
|
+
import { httpRouter } from '../wirings/http/routers/http-router.js'
|
|
10
|
+
import type { Logger } from '../services/logger.js'
|
|
11
|
+
import type { SchemaService } from '../services/schema-service.js'
|
|
12
|
+
|
|
13
|
+
export interface ReloadGeneratedMetaOptions {
|
|
14
|
+
/** The project's generated output directory (the CLI's resolved outDir). */
|
|
15
|
+
pikkuDir: string
|
|
16
|
+
logger: Logger
|
|
17
|
+
/** Used to recompile validators for changed schemas; falls back to the
|
|
18
|
+
* schema service on the registered singleton services. */
|
|
19
|
+
schemaService?: SchemaService
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const readJson = async (
|
|
23
|
+
logger: Logger,
|
|
24
|
+
file: string
|
|
25
|
+
): Promise<any | undefined> => {
|
|
26
|
+
let raw: string
|
|
27
|
+
try {
|
|
28
|
+
raw = await readFile(file, 'utf-8')
|
|
29
|
+
} catch {
|
|
30
|
+
// The project doesn't use this wiring type — nothing generated.
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(raw)
|
|
35
|
+
} catch (err) {
|
|
36
|
+
logger.error(
|
|
37
|
+
`Hot-reload could not parse ${file}: ${err instanceof Error ? err.message : String(err)}`
|
|
38
|
+
)
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Re-reads the codegen output (wiring meta + JSON schemas) into the running
|
|
45
|
+
* process so new and changed functions become callable without a server
|
|
46
|
+
* restart. The generated `*-meta.gen.ts` files are plain
|
|
47
|
+
* `pikkuState(area, key, <json>)` side effects, but they cannot be
|
|
48
|
+
* re-imported after a dev-time codegen — the ESM cache pins both the wrapper
|
|
49
|
+
* and its JSON import — so this reads the JSON sources directly and applies
|
|
50
|
+
* the same state.
|
|
51
|
+
*
|
|
52
|
+
* Meant to be called by a dev-server watcher after each codegen pass. Routes
|
|
53
|
+
* registered by NEW `wireHTTP` files are not picked up (their modules were
|
|
54
|
+
* never imported); those still need a restart.
|
|
55
|
+
*/
|
|
56
|
+
export async function reloadGeneratedMeta(
|
|
57
|
+
options: ReloadGeneratedMetaOptions
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const { pikkuDir, logger, schemaService } = options
|
|
60
|
+
const dir = resolve(pikkuDir)
|
|
61
|
+
|
|
62
|
+
const functionsMeta = await readJson(
|
|
63
|
+
logger,
|
|
64
|
+
join(dir, 'function/pikku-functions-meta.gen.json')
|
|
65
|
+
)
|
|
66
|
+
// Merge over the existing map, don't replace it: framework internals like
|
|
67
|
+
// pikkuWorkflowOrchestrator / the per-workflow queue workers are registered
|
|
68
|
+
// at service-init (pikku-workflow-service.ts), never in the generated JSON —
|
|
69
|
+
// a wholesale replace drops them and workflow jobs then fail with
|
|
70
|
+
// "Function meta not found: pikkuWorkflowOrchestrator".
|
|
71
|
+
if (functionsMeta) {
|
|
72
|
+
const existing = pikkuState(null, 'function', 'meta') ?? {}
|
|
73
|
+
pikkuState(null, 'function', 'meta', { ...existing, ...functionsMeta })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const httpMeta = await readJson(
|
|
77
|
+
logger,
|
|
78
|
+
join(dir, 'http/pikku-http-wirings-meta.gen.json')
|
|
79
|
+
)
|
|
80
|
+
if (httpMeta) pikkuState(null, 'http', 'meta', httpMeta)
|
|
81
|
+
|
|
82
|
+
const rpcMeta = await readJson(
|
|
83
|
+
logger,
|
|
84
|
+
join(dir, 'rpc/pikku-rpc-wirings-meta.internal.gen.json')
|
|
85
|
+
)
|
|
86
|
+
if (rpcMeta) pikkuState(null, 'rpc', 'meta', rpcMeta)
|
|
87
|
+
|
|
88
|
+
const queueMeta = await readJson(
|
|
89
|
+
logger,
|
|
90
|
+
join(dir, 'queue/pikku-queue-workers-wirings-meta.gen.json')
|
|
91
|
+
)
|
|
92
|
+
// Same reason as function meta: the workflow service adds its orchestrator /
|
|
93
|
+
// step queues (and wf-orchestrator-* / wf-step-* per-workflow queues) here at
|
|
94
|
+
// init, absent from the generated JSON — merge so they survive the reload.
|
|
95
|
+
if (queueMeta) {
|
|
96
|
+
const existing = pikkuState(null, 'queue', 'meta') ?? {}
|
|
97
|
+
pikkuState(null, 'queue', 'meta', { ...existing, ...queueMeta })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const agentMeta = await readJson(
|
|
101
|
+
logger,
|
|
102
|
+
join(dir, 'agent/pikku-agent-wirings-meta.gen.json')
|
|
103
|
+
)
|
|
104
|
+
if (agentMeta?.agentsMeta) {
|
|
105
|
+
pikkuState(null, 'agent', 'agentsMeta', agentMeta.agentsMeta)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Generated JSON schemas: <outDir>/schemas/schemas/<Name>.schema.json.
|
|
109
|
+
// Re-adding replaces the map entry; the schema service recompiles any
|
|
110
|
+
// validator whose stored schema value no longer matches.
|
|
111
|
+
const schemasDir = join(dir, 'schemas', 'schemas')
|
|
112
|
+
let schemaFiles: string[] = []
|
|
113
|
+
try {
|
|
114
|
+
schemaFiles = await readdir(schemasDir)
|
|
115
|
+
} catch {
|
|
116
|
+
// No generated schemas — a schema-less project.
|
|
117
|
+
}
|
|
118
|
+
for (const file of schemaFiles) {
|
|
119
|
+
if (!file.endsWith('.schema.json')) continue
|
|
120
|
+
const schema = await readJson(logger, join(schemasDir, file))
|
|
121
|
+
if (schema) {
|
|
122
|
+
addSchema(file.slice(0, -'.schema.json'.length), schema)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
clearMiddlewareCache()
|
|
127
|
+
clearPermissionsCache()
|
|
128
|
+
clearChannelMiddlewareCache()
|
|
129
|
+
httpRouter.reset()
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
compileAllSchemas(logger, schemaService)
|
|
133
|
+
} catch (err) {
|
|
134
|
+
logger.error(
|
|
135
|
+
`Hot-reload schema recompilation failed: ${err instanceof Error ? err.message : String(err)}`
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|