@pikku/cli 0.12.44 → 0.12.46
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/cli.schema.json +1 -1
- package/console-app/assets/index-ByiKUJ11.js +254 -0
- package/console-app/index.html +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +5 -0
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +5 -0
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.json +474 -0
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +14 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +17 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +51 -51
- package/dist/.pikku/function/pikku-functions.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.json +8 -8
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +1 -1
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/meta/allWorkflow.gen.json +15 -15
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/deploy/build-pipeline.js +2 -0
- package/dist/src/deploy/bundler/bundler.d.ts +1 -0
- package/dist/src/deploy/bundler/bundler.js +36 -5
- package/dist/src/deploy/provider-adapter.d.ts +7 -0
- package/dist/src/fabric/functions/validate.function.js +185 -0
- package/dist/src/functions/wirings/auth/pikku-command-auth.js +10 -1
- package/dist/src/functions/wirings/channels/pikku-channels.js +20 -11
- package/dist/src/functions/wirings/channels/pikku-command-channels-map.js +2 -2
- package/dist/src/functions/wirings/channels/pikku-command-channels.js +20 -6
- package/dist/src/functions/wirings/channels/serialize-typed-channel-map.d.ts +1 -1
- package/dist/src/functions/wirings/channels/serialize-typed-channel-map.js +59 -11
- package/dist/src/functions/wirings/cli/pikku-command-cli.js +20 -6
- package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +7 -1
- package/dist/src/functions/wirings/functions/serialize-addon-refs.d.ts +14 -0
- package/dist/src/functions/wirings/functions/serialize-addon-refs.js +129 -0
- package/dist/src/functions/wirings/http/pikku-command-http-routes.js +21 -6
- package/dist/src/functions/wirings/http/serialize-typed-http-map.js +12 -0
- package/dist/src/functions/workflows/all.workflow.js +7 -0
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/dist/src/utils/pikku-cli-config.js +18 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/skills/pikku-better-auth/SKILL.md +19 -3
- package/console-app/assets/index-CRLT8CXr.js +0 -254
|
@@ -349,7 +349,7 @@
|
|
|
349
349
|
"HTTP": {
|
|
350
350
|
"nodeId": "HTTP",
|
|
351
351
|
"rpcName": "pikkuCommandHTTP",
|
|
352
|
-
"next": "
|
|
352
|
+
"next": "Channels",
|
|
353
353
|
"stepHash": "ba1b4f85fc79"
|
|
354
354
|
},
|
|
355
355
|
"Scheduler": {
|
|
@@ -397,9 +397,21 @@
|
|
|
397
397
|
"Realtime client": {
|
|
398
398
|
"nodeId": "Realtime client",
|
|
399
399
|
"rpcName": "pikkuRealtime",
|
|
400
|
-
"next": "
|
|
400
|
+
"next": "HTTP",
|
|
401
401
|
"stepHash": "3aa3265e392d"
|
|
402
402
|
},
|
|
403
|
+
"Channels": {
|
|
404
|
+
"nodeId": "Channels",
|
|
405
|
+
"rpcName": "pikkuCommandChannels",
|
|
406
|
+
"next": "Gateway",
|
|
407
|
+
"stepHash": "aae49a4acd3c"
|
|
408
|
+
},
|
|
409
|
+
"CLI": {
|
|
410
|
+
"nodeId": "CLI",
|
|
411
|
+
"rpcName": "pikkuCLI",
|
|
412
|
+
"next": "Queue map",
|
|
413
|
+
"stepHash": "8b5a8e81446a"
|
|
414
|
+
},
|
|
403
415
|
"Functions": {
|
|
404
416
|
"nodeId": "Functions",
|
|
405
417
|
"rpcName": "pikkuFunctions",
|
|
@@ -412,12 +424,6 @@
|
|
|
412
424
|
"next": "Channels",
|
|
413
425
|
"stepHash": "fba2e19d515a"
|
|
414
426
|
},
|
|
415
|
-
"Channels": {
|
|
416
|
-
"nodeId": "Channels",
|
|
417
|
-
"rpcName": "pikkuCommandChannels",
|
|
418
|
-
"next": "Gateway",
|
|
419
|
-
"stepHash": "aae49a4acd3c"
|
|
420
|
-
},
|
|
421
427
|
"Gateway": {
|
|
422
428
|
"nodeId": "Gateway",
|
|
423
429
|
"rpcName": "pikkuGateway",
|
|
@@ -430,12 +436,6 @@
|
|
|
430
436
|
"next": "CLI",
|
|
431
437
|
"stepHash": "fd4e9c89c777"
|
|
432
438
|
},
|
|
433
|
-
"CLI": {
|
|
434
|
-
"nodeId": "CLI",
|
|
435
|
-
"rpcName": "pikkuCLI",
|
|
436
|
-
"next": "Queue map",
|
|
437
|
-
"stepHash": "8b5a8e81446a"
|
|
438
|
-
},
|
|
439
439
|
"Queue map": {
|
|
440
440
|
"nodeId": "Queue map",
|
|
441
441
|
"rpcName": "pikkuCommandQueueMap",
|
|
@@ -545,5 +545,5 @@
|
|
|
545
545
|
"entryNodeIds": [
|
|
546
546
|
"Bootstrap inspect"
|
|
547
547
|
],
|
|
548
|
-
"graphHash": "
|
|
548
|
+
"graphHash": "e6e8c0ae99f8"
|
|
549
549
|
}
|
package/dist/bin/pikku-bin.mjs
CHANGED
|
@@ -11,8 +11,8 @@ async function checkForUpdate() {
|
|
|
11
11
|
})
|
|
12
12
|
if (!res.ok) return
|
|
13
13
|
const { version: latest } = await res.json()
|
|
14
|
-
if (latest !== '0.12.
|
|
15
|
-
process.stderr.write(`\n Update available 0.12.
|
|
14
|
+
if (latest !== '0.12.46') {
|
|
15
|
+
process.stderr.write(`\n Update available 0.12.46 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
|
|
16
16
|
}
|
|
17
17
|
} catch {}
|
|
18
18
|
}
|
|
@@ -81,6 +81,7 @@ export async function runBuildPipeline(options) {
|
|
|
81
81
|
entryFiles.set(unitName, entryPath);
|
|
82
82
|
const bundleResult = await bundleUnits(projectDir, manifest, entryFiles, providerDir, {
|
|
83
83
|
externals: provider.getExternals?.(),
|
|
84
|
+
stubModules: provider.getStubModules?.(),
|
|
84
85
|
aliases: provider.getAliases?.(),
|
|
85
86
|
define: provider.getDefine?.(),
|
|
86
87
|
platform: provider.getPlatform?.(),
|
|
@@ -206,6 +207,7 @@ export async function runBuildPipeline(options) {
|
|
|
206
207
|
};
|
|
207
208
|
const result = await bundleUnits(projectDir, serverlessManifestForBundle, serverlessEntryFiles, providerDir, {
|
|
208
209
|
externals: provider.getExternals?.(),
|
|
210
|
+
stubModules: provider.getStubModules?.(),
|
|
209
211
|
aliases: provider.getAliases?.(),
|
|
210
212
|
define: provider.getDefine?.(),
|
|
211
213
|
platform: provider.getPlatform?.(),
|
|
@@ -30,5 +30,6 @@ export declare function bundleUnits(projectDir: string, manifest: DeploymentMani
|
|
|
30
30
|
noRequireShim?: boolean;
|
|
31
31
|
sourcemap?: boolean;
|
|
32
32
|
emitMetafile?: boolean;
|
|
33
|
+
stubModules?: string[];
|
|
33
34
|
resolveOutputDir?: (unit: DeploymentUnit, baseOutputDir: string) => string;
|
|
34
35
|
}): Promise<BundleOutput>;
|
|
@@ -21,6 +21,21 @@ import { extractDependencies, generateMinimalPackageJson, } from './dep-extracto
|
|
|
21
21
|
const SERVICE_GEN_FILE_MAP = {
|
|
22
22
|
metaService: /pikku-meta-service\.gen/,
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Mapping of service name -> npm module patterns to stub when the service is
|
|
26
|
+
* NOT required by a deployment unit. Unlike SERVICE_GEN_FILE_MAP these are
|
|
27
|
+
* external packages, not gen files: a unit that doesn't wire the service never
|
|
28
|
+
* executes the code path that imports them, so replacing them with `export {}`
|
|
29
|
+
* keeps their (often large) trees out of the bundle.
|
|
30
|
+
*
|
|
31
|
+
* The AI SDKs (@pikku/ai-vercel + @ai-sdk/* + `ai`, ~3MB) are only constructed
|
|
32
|
+
* when `aiAgentRunner` is wired (agent units). Every non-agent unit stubs them.
|
|
33
|
+
* The shared services factory must guard the runner construction behind a
|
|
34
|
+
* defined-check on the dynamic import so a stubbed unit simply skips it.
|
|
35
|
+
*/
|
|
36
|
+
const SERVICE_MODULE_MAP = {
|
|
37
|
+
aiAgentRunner: [/^@pikku\/ai-vercel/, /^@ai-sdk\//, /^ai$/],
|
|
38
|
+
};
|
|
24
39
|
/**
|
|
25
40
|
* Read the per-unit pikku-services.gen.ts and return the set of gen file
|
|
26
41
|
* patterns that should be stubbed (because their service is not required).
|
|
@@ -34,8 +49,14 @@ async function getDeadGenFilePatterns(unitOutputDir) {
|
|
|
34
49
|
if (match) {
|
|
35
50
|
for (const line of match[1].split('\n')) {
|
|
36
51
|
const kv = line.match(/'([^']+)':\s*false/);
|
|
37
|
-
if (kv
|
|
38
|
-
|
|
52
|
+
if (!kv)
|
|
53
|
+
continue;
|
|
54
|
+
const service = kv[1];
|
|
55
|
+
if (SERVICE_GEN_FILE_MAP[service]) {
|
|
56
|
+
patterns.push(SERVICE_GEN_FILE_MAP[service]);
|
|
57
|
+
}
|
|
58
|
+
if (SERVICE_MODULE_MAP[service]) {
|
|
59
|
+
patterns.push(...SERVICE_MODULE_MAP[service]);
|
|
39
60
|
}
|
|
40
61
|
}
|
|
41
62
|
}
|
|
@@ -81,14 +102,20 @@ const EXACT_DEPENDENCIES_FILENAME = 'exact-dependencies.json';
|
|
|
81
102
|
* - package.json: Minimal manifest with only the external deps this unit needs
|
|
82
103
|
*/
|
|
83
104
|
async function bundleUnit(options) {
|
|
84
|
-
const { unit, entryPath, unitOutputDir, projectDir, externals, aliases, define, platform, format, sourcemap, emitMetafile, } = options;
|
|
105
|
+
const { unit, entryPath, unitOutputDir, projectDir, externals, aliases, define, platform, format, sourcemap, emitMetafile, stubModules, } = options;
|
|
85
106
|
await mkdir(unitOutputDir, { recursive: true });
|
|
86
107
|
const bundlePath = join(unitOutputDir, BUNDLE_FILENAME);
|
|
87
108
|
const metafilePath = join(unitOutputDir, METAFILE_FILENAME);
|
|
88
109
|
const packageJsonPath = join(unitOutputDir, PACKAGE_JSON_FILENAME);
|
|
89
110
|
const exactDependenciesPath = join(unitOutputDir, EXACT_DEPENDENCIES_FILENAME);
|
|
90
|
-
// Determine which gen files to stub based on per-unit service requirements
|
|
111
|
+
// Determine which gen files to stub based on per-unit service requirements,
|
|
112
|
+
// plus any provider-supplied module stubs (modules the provider's runtime
|
|
113
|
+
// never executes — e.g. the `postgres` driver on CF Workers, which use a
|
|
114
|
+
// libsql/Turso dialect; the postgres branch is URL-gated and never taken).
|
|
91
115
|
const deadPatterns = await getDeadGenFilePatterns(unitOutputDir);
|
|
116
|
+
for (const source of stubModules ?? []) {
|
|
117
|
+
deadPatterns.push(new RegExp(source));
|
|
118
|
+
}
|
|
92
119
|
// Run esbuild — inline everything into a self-contained bundle.
|
|
93
120
|
// Only Node built-ins are kept external (CF Workers provides them).
|
|
94
121
|
// The stub plugin replaces gen files for unused services with empty
|
|
@@ -132,7 +159,11 @@ async function bundleUnit(options) {
|
|
|
132
159
|
metafile: true,
|
|
133
160
|
target: 'es2022',
|
|
134
161
|
outfile: bundlePath,
|
|
135
|
-
|
|
162
|
+
// Minify every deploy bundle — esbuild output ships straight to the runtime
|
|
163
|
+
// (CF Workers / container), tsc is never the bundler. keepNames preserves
|
|
164
|
+
// Function.name / constructor.name so name-based reflection still works.
|
|
165
|
+
minify: true,
|
|
166
|
+
keepNames: true,
|
|
136
167
|
sourcemap: sourcemap ?? false,
|
|
137
168
|
logLevel: 'warning',
|
|
138
169
|
loader: { '.ts': 'ts' },
|
|
@@ -74,6 +74,13 @@ export interface ProviderAdapter {
|
|
|
74
74
|
* Defaults to ['node:*'] if not provided.
|
|
75
75
|
*/
|
|
76
76
|
getExternals?(): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Regex sources for modules to stub to `export {}` during bundling — modules
|
|
79
|
+
* this provider's runtime never executes (e.g. the `postgres` driver on CF
|
|
80
|
+
* Workers, which use a libsql/Turso dialect). Unlike `getExternals`, a stub
|
|
81
|
+
* removes the bytes entirely rather than leaving a runtime import to resolve.
|
|
82
|
+
*/
|
|
83
|
+
getStubModules?(): string[];
|
|
77
84
|
/**
|
|
78
85
|
* Module aliases for esbuild bundling (e.g. { crypto: 'node:crypto' }).
|
|
79
86
|
* Used to remap bare imports to platform-compatible paths.
|
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { readFile, readdir } from 'node:fs/promises';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import { pikkuSessionlessFunc } from '../../../.pikku/pikku-types.gen.js';
|
|
6
7
|
import { added, changed, removed, dim } from '../lib/output.js';
|
|
7
8
|
const FindingSchema = z.object({
|
|
@@ -59,6 +60,35 @@ async function readTextSafe(path) {
|
|
|
59
60
|
return null;
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
// List .ts/.tsx source files under a directory (skips node_modules). Used to
|
|
64
|
+
// scan an app for raw @mantine/core imports and i18n usage.
|
|
65
|
+
async function listSourceFiles(dir) {
|
|
66
|
+
if (!existsSync(dir))
|
|
67
|
+
return [];
|
|
68
|
+
try {
|
|
69
|
+
return (await readdir(dir, { recursive: true }))
|
|
70
|
+
.filter((f) => typeof f === 'string' &&
|
|
71
|
+
(f.endsWith('.ts') || f.endsWith('.tsx')) &&
|
|
72
|
+
!f.includes('node_modules'))
|
|
73
|
+
.map((f) => join(dir, f));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Module-singleton-sensitive packages: a SECOND physical copy splits
|
|
80
|
+
// module-level state. The TanStack Start dev server registers its SSR
|
|
81
|
+
// middleware on one copy of @tanstack/start-plugin-core while the config hook
|
|
82
|
+
// reads another, so the frontend serves "Cannot GET /" (404). React/react-dom
|
|
83
|
+
// duplicates break hooks. This is a workspace-hoisting artifact, not a version
|
|
84
|
+
// mismatch — `resolutions` pins do NOT collapse it. Curated, not exhaustive:
|
|
85
|
+
// most duplicate deps are harmless, so only these are checked.
|
|
86
|
+
const SINGLETON_SENSITIVE_PKGS = [
|
|
87
|
+
'vite',
|
|
88
|
+
'@tanstack/start-plugin-core',
|
|
89
|
+
'react',
|
|
90
|
+
'react-dom',
|
|
91
|
+
];
|
|
62
92
|
// Minimum @pikku/* versions Fabric requires. The pikku packages are versioned
|
|
63
93
|
// independently (e.g. @pikku/cli moves faster than @pikku/core), so this is a
|
|
64
94
|
// per-package floor map, not a single number. Only listed packages are
|
|
@@ -318,6 +348,46 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
318
348
|
// readdir failure — skip
|
|
319
349
|
}
|
|
320
350
|
}
|
|
351
|
+
// ── better-auth stateless session (unit tree-shaking) ──────────────────
|
|
352
|
+
// Without `session.cookieCache`, the CLI wires the STATEFUL betterAuthSession
|
|
353
|
+
// bridge globally — every non-auth unit then bundles the full better-auth
|
|
354
|
+
// server (~2.5MB each), bloating bundles and the serial deploy uploads.
|
|
355
|
+
// Enabling cookieCache splits out a lean betterAuthStatelessSession that
|
|
356
|
+
// verifies the signed cookie, so only the auth unit carries the server. A
|
|
357
|
+
// hand-written global betterAuthSession defeats it the same way.
|
|
358
|
+
const fnSrcDir = join(fnDir, 'src');
|
|
359
|
+
if (existsSync(fnSrcDir)) {
|
|
360
|
+
try {
|
|
361
|
+
const srcFiles = (await readdir(fnSrcDir, { recursive: true })).filter((f) => typeof f === 'string' &&
|
|
362
|
+
(f.endsWith('.ts') || f.endsWith('.tsx')) &&
|
|
363
|
+
!f.endsWith('.gen.ts') &&
|
|
364
|
+
!f.includes('node_modules'));
|
|
365
|
+
for (const rel of srcFiles) {
|
|
366
|
+
const full = join(fnSrcDir, rel);
|
|
367
|
+
const text = await readTextSafe(full);
|
|
368
|
+
if (!text)
|
|
369
|
+
continue;
|
|
370
|
+
// 1) better-auth config without cookieCache enabled.
|
|
371
|
+
if (/\bpikkuBetterAuth\s*\(/.test(text) &&
|
|
372
|
+
/\bbetterAuth\s*\(/.test(text)) {
|
|
373
|
+
const cookieCacheDisabled = !/cookieCache/.test(text) ||
|
|
374
|
+
/cookieCache\s*:\s*\{[^}]*enabled\s*:\s*false/.test(text);
|
|
375
|
+
if (cookieCacheDisabled) {
|
|
376
|
+
w('better-auth-stateless-session-disabled', 'better-auth config does not enable session.cookieCache — every non-auth unit bundles the full better-auth server (~2.5MB each), bloating bundles and the serial deploy uploads', full, 'Add `session: { cookieCache: { enabled: true } }` to the betterAuth({...}) config so the CLI splits out betterAuthStatelessSession (pikku #737)');
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// 2) hand-written global stateful betterAuthSession bridge.
|
|
380
|
+
if (/addHTTPMiddleware\s*\(\s*['"`]\*['"`]/.test(text) &&
|
|
381
|
+
/\bbetterAuthSession\s*\(/.test(text) &&
|
|
382
|
+
!/betterAuthStatelessSession/.test(text)) {
|
|
383
|
+
w('better-auth-stateful-session-global', 'a global addHTTPMiddleware registers the stateful betterAuthSession bridge — it pulls the full better-auth server into every unit, defeating stateless tree-shaking', full, 'Switch to betterAuthStatelessSession (requires session.cookieCache). A custom mapSession is currently pre-empted by the CLI-generated stateless middleware — see pikku #754');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// readdir failure — skip
|
|
389
|
+
}
|
|
390
|
+
}
|
|
321
391
|
// Database layout is declared by pikku.config.json db.engine.
|
|
322
392
|
const migrationsDir = join(root, 'db', dbEngine === 'postgres' ? 'postgres' : 'sqlite');
|
|
323
393
|
if (!existsSync(migrationsDir)) {
|
|
@@ -449,6 +519,121 @@ export async function runValidate(startDir = process.cwd()) {
|
|
|
449
519
|
if (componentsPkgName && !appDeps[componentsPkgName]) {
|
|
450
520
|
info(`app-missing-components-${name}`, `apps/${name} does not depend on ${componentsPkgName}`, join(appPath, 'package.json'), `Add "${componentsPkgName}: workspace:*" to apps/${name}/package.json dependencies`);
|
|
451
521
|
}
|
|
522
|
+
// The scaffolded dev vite config (generate-frontend-runtime) imports
|
|
523
|
+
// @babel/core to tag JSX with data-om-id for alt-click design editing.
|
|
524
|
+
// It resolves transitively via @vitejs/plugin-react, but that's a silent
|
|
525
|
+
// dependency — declare it explicitly so the resolution can't drift away.
|
|
526
|
+
if (!appPkg.devDependencies?.['@babel/core']) {
|
|
527
|
+
w(`app-missing-babel-core-${name}`, `apps/${name} does not declare @babel/core — the dev runtime needs it to instrument JSX (data-om-id) for design alt-click`, join(appPath, 'package.json'), `Add "@babel/core": "^7.26.0" to apps/${name}/package.json devDependencies`);
|
|
528
|
+
}
|
|
529
|
+
// ── i18n + @pikku/mantine convergence (React frontend apps) ──────────
|
|
530
|
+
// Every frontend converges onto the canonical starter-template stack:
|
|
531
|
+
// Paraglide JS (inlang) for translation + components imported from
|
|
532
|
+
// @pikku/mantine/core (whose I18nNode-typed props make untranslated
|
|
533
|
+
// strings a compile error). A raw @mantine/core import bypasses that gate.
|
|
534
|
+
// The i18next → Paraglide cutover is hard (no back-compat), so a residual
|
|
535
|
+
// i18next dep or useTranslation()/useI18n() call is an error.
|
|
536
|
+
const appAllDeps = {
|
|
537
|
+
...appPkg.dependencies,
|
|
538
|
+
...appPkg.devDependencies,
|
|
539
|
+
};
|
|
540
|
+
const isReactFrontend = !!(appAllDeps['@mantine/core'] ||
|
|
541
|
+
appAllDeps['@pikku/mantine'] ||
|
|
542
|
+
appAllDeps['react']);
|
|
543
|
+
if (isReactFrontend) {
|
|
544
|
+
const srcFiles = await listSourceFiles(join(appPath, 'src'));
|
|
545
|
+
let usesMessages = false;
|
|
546
|
+
const rawMantineFiles = [];
|
|
547
|
+
const legacyI18nFiles = [];
|
|
548
|
+
for (const file of srcFiles) {
|
|
549
|
+
const text = await readTextSafe(file);
|
|
550
|
+
if (!text)
|
|
551
|
+
continue;
|
|
552
|
+
const rel = file.slice(appPath.length + 1);
|
|
553
|
+
const norm = rel.replace(/\\/g, '/');
|
|
554
|
+
// Paraglide usage: the reactive useLocale() hook or an import from the
|
|
555
|
+
// local `@/i18n` scaffold (messages `m`, mKey/mList) — either means
|
|
556
|
+
// strings flow through compiled messages.
|
|
557
|
+
if (/\buseLocale\s*\(/.test(text) ||
|
|
558
|
+
/from\s+['"]@\/i18n(?:\/[\w-]+)?['"]/.test(text)) {
|
|
559
|
+
usesMessages = true;
|
|
560
|
+
}
|
|
561
|
+
// Legacy i18next/react-i18next/@pikku/react-i18n markers — removed by
|
|
562
|
+
// the cutover. The scaffold's own config.ts names these in comments,
|
|
563
|
+
// so skip src/i18n/ and match imports/hook calls, not bare words.
|
|
564
|
+
if (!/(?:^|\/)i18n\//.test(norm) &&
|
|
565
|
+
(/from\s+['"](?:react-i18next|i18next|@pikku\/react\/i18n)['"]/.test(text) ||
|
|
566
|
+
/\buseTranslation\s*\(/.test(text) ||
|
|
567
|
+
/\buseI18n\s*\(/.test(text))) {
|
|
568
|
+
legacyI18nFiles.push(rel);
|
|
569
|
+
}
|
|
570
|
+
// component import from @mantine/core — the trailing quote excludes
|
|
571
|
+
// the `@mantine/core/styles.css` side-effect import and @mantine/hooks
|
|
572
|
+
if (/from\s+['"]@mantine\/core['"]/.test(text)) {
|
|
573
|
+
rawMantineFiles.push(rel);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const hasParaglideDep = !!appAllDeps['@inlang/paraglide-js'];
|
|
577
|
+
const hasMessagesDir = existsSync(join(appPath, 'messages'));
|
|
578
|
+
const hasInlangProject = existsSync(join(appPath, 'project.inlang', 'settings.json'));
|
|
579
|
+
const hasLegacyI18nDeps = !!(appAllDeps['i18next'] || appAllDeps['react-i18next']);
|
|
580
|
+
// 1) i18next must be fully removed — hard cutover to Paraglide.
|
|
581
|
+
if (hasLegacyI18nDeps) {
|
|
582
|
+
e(`app-legacy-i18next-dep-${name}`, `apps/${name} still depends on i18next/react-i18next — Fabric migrated to Paraglide JS (inlang); the i18next stack must be removed`, join(appPath, 'package.json'), lines('Remove "i18next", "react-i18next" and "i18next-browser-languagedetector".', 'Add "@inlang/paraglide-js" (devDependencies) and the src/i18n scaffold.', 'Reference: templates/starter-template/apps/app.'));
|
|
583
|
+
}
|
|
584
|
+
if (legacyI18nFiles.length > 0) {
|
|
585
|
+
e(`app-legacy-i18n-usage-${name}`, `apps/${name} still calls useTranslation()/useI18n() or imports i18next in ${legacyI18nFiles.length} file(s) — these are removed by the Paraglide cutover`, join(appPath, 'src'), lines('Convert legacy i18n usage to Paraglide in:', ...legacyI18nFiles.slice(0, 10).map((f) => ` - ${f}`), ...(legacyI18nFiles.length > 10
|
|
586
|
+
? [` …and ${legacyI18nFiles.length - 10} more`]
|
|
587
|
+
: []), "Replace `const { t } = useTranslation()` with `useLocale()` from '@/i18n/config',", "and `t('a.b')` with `m.a_b()` from '@/i18n/messages'."));
|
|
588
|
+
}
|
|
589
|
+
// 2) Paraglide must be present and wired (messages + inlang project).
|
|
590
|
+
if (!hasParaglideDep) {
|
|
591
|
+
e(`app-missing-paraglide-${name}`, `apps/${name} has no Paraglide i18n stack — every Fabric frontend must be translatable`, join(appPath, 'package.json'), lines('Add the canonical Paraglide stack:', '1. devDep: "@inlang/paraglide-js".', '2. messages/<locale>.json + project.inlang/settings.json (snake_case keys).', '3. src/i18n scaffold: config.ts (useLocale), messages.ts (branded `m`), ident.ts.', '4. vite.config: paraglideVitePlugin({ project: "./project.inlang", outdir: "./src/paraglide" }).', 'Route every user-visible string through `m.*()`; reference templates/starter-template/apps/app/src/i18n.'));
|
|
592
|
+
}
|
|
593
|
+
else if (!hasMessagesDir || !hasInlangProject) {
|
|
594
|
+
e(`app-paraglide-not-wired-${name}`, `apps/${name} declares @inlang/paraglide-js but is missing ${!hasMessagesDir ? 'messages/' : 'project.inlang/settings.json'} — Paraglide cannot compile`, appPath, lines('Paraglide compiles `messages/<locale>.json` against `project.inlang/settings.json`.', 'Create both (snake_case keys) — the generated src/paraglide/ output is gitignored.'));
|
|
595
|
+
}
|
|
596
|
+
else if (!usesMessages && srcFiles.length > 0) {
|
|
597
|
+
w(`app-i18n-unused-${name}`, `apps/${name} ships Paraglide but no component imports from @/i18n or calls useLocale() — strings are not actually translated`, appPath, "Route user-visible strings through `m.*()` from '@/i18n/messages' and subscribe via `useLocale()`.");
|
|
598
|
+
}
|
|
599
|
+
if (!appAllDeps['@pikku/mantine'] && appAllDeps['@mantine/core']) {
|
|
600
|
+
e(`app-missing-pikku-mantine-${name}`, `apps/${name} uses @mantine/core but not @pikku/mantine — components bypass the i18n-typed compile gate`, join(appPath, 'package.json'), 'Add "@pikku/mantine": "^0.12.5" and import components from "@pikku/mantine/core" (a drop-in for @mantine/core with I18nNode-typed string props).');
|
|
601
|
+
}
|
|
602
|
+
if (rawMantineFiles.length > 0) {
|
|
603
|
+
e(`app-raw-mantine-imports-${name}`, `apps/${name} imports components from "@mantine/core" directly in ${rawMantineFiles.length} file(s) — this bypasses the @pikku/mantine i18n gate, so untranslated strings compile silently`, join(appPath, 'src'), lines(`Swap 'from "@mantine/core"' → 'from "@pikku/mantine/core"' in:`, ...rawMantineFiles.slice(0, 10).map((f) => ` - ${f}`), ...(rawMantineFiles.length > 10
|
|
604
|
+
? [` …and ${rawMantineFiles.length - 10} more`]
|
|
605
|
+
: []), 'Keep "@mantine/core/styles.css", @mantine/hooks and @mantine/notifications imports as-is.'));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
// ── singleton-sensitive deps must resolve to ONE physical copy ─────────
|
|
610
|
+
// A second physical copy of a peer-virtualized lib (or React) splits
|
|
611
|
+
// module-level state and breaks TanStack Start dev SSR — the perauset
|
|
612
|
+
// "Cannot GET /" 404. Invariant: one resolved install dir per package
|
|
613
|
+
// across {app, root}. Best-effort: needs node_modules installed; anything
|
|
614
|
+
// unresolvable is skipped.
|
|
615
|
+
for (const name of appEntries) {
|
|
616
|
+
const appPath = join(appsDir, name);
|
|
617
|
+
if (!existsSync(join(appPath, 'package.json')))
|
|
618
|
+
continue;
|
|
619
|
+
for (const pkg of SINGLETON_SENSITIVE_PKGS) {
|
|
620
|
+
const installDirs = new Set();
|
|
621
|
+
for (const base of [appPath, root]) {
|
|
622
|
+
try {
|
|
623
|
+
const resolved = createRequire(join(base, 'package.json')).resolve(pkg);
|
|
624
|
+
const esc = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
625
|
+
const m = resolved.match(new RegExp(`^(.*[\\\\/]node_modules[\\\\/]${esc})[\\\\/]`));
|
|
626
|
+
if (m)
|
|
627
|
+
installDirs.add(m[1]);
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// not resolvable from this base — skip
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (installDirs.size > 1) {
|
|
634
|
+
e(`dup-physical-copy-${name}-${pkg.replace(/[@/]/g, '-')}`, `apps/${name}: "${pkg}" resolves to ${installDirs.size} distinct physical copies — a module-singleton split (breaks TanStack Start dev SSR → frontend 404)`, appPath, lines(`"${pkg}" is installed more than once (e.g. one hoisted to the repo root and one nested under apps/${name}).`, `Declare "${pkg}" in exactly ONE workspace manifest (the root OR apps/${name}, not both), delete yarn.lock, and reinstall so it hoists to a single copy.`, '`resolutions` version-pins do NOT collapse a peer-virtualized duplicate.'));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
452
637
|
}
|
|
453
638
|
}
|
|
454
639
|
// ── packages/theme + packages/components ──────────────────────────────
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { join, dirname } from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { rm } from 'node:fs/promises';
|
|
2
4
|
import { pikkuSessionlessFunc } from '#pikku';
|
|
3
5
|
import { writeFileInDir } from '../../../utils/file-writer.js';
|
|
4
6
|
import { logCommandInfoAndTime } from '../../../middleware/log-command-info-and-time.js';
|
|
@@ -24,10 +26,17 @@ export const pikkuAuth = pikkuSessionlessFunc({
|
|
|
24
26
|
await writeFileInDir(logger, authFile, wiring);
|
|
25
27
|
await writeFileInDir(logger, secretsFile, secrets);
|
|
26
28
|
// Stateless split: session middleware in its own file (see serializeAuthGen).
|
|
29
|
+
// Skip it when the project registers its own betterAuthStatelessSession — the
|
|
30
|
+
// generated default-map one would run first and pre-empt the user's custom
|
|
31
|
+
// mapSession (pikkujs/pikku#754). Remove a stale file so it can't linger and
|
|
32
|
+
// double-register.
|
|
27
33
|
const middlewareFile = join(dirname(authFile), 'auth-middleware.gen.ts');
|
|
28
|
-
if (middleware) {
|
|
34
|
+
if (middleware && !state.auth.userStatelessSession) {
|
|
29
35
|
await writeFileInDir(logger, middlewareFile, middleware);
|
|
30
36
|
}
|
|
37
|
+
else if (existsSync(middlewareFile)) {
|
|
38
|
+
await rm(middlewareFile, { force: true });
|
|
39
|
+
}
|
|
31
40
|
// Static metadata of the enabled providers/plugins for the console SSO page,
|
|
32
41
|
// following the `*-meta.gen.json` convention. Read at runtime by the console
|
|
33
42
|
// getAuthProviders function instead of a runtime registry.
|
|
@@ -6,19 +6,28 @@ import { getFileImportRelativePath } from '../../../utils/file-import-path.js';
|
|
|
6
6
|
export const pikkuChannels = pikkuVoidFunc({
|
|
7
7
|
func: async ({ logger, config, getInspectorState }) => {
|
|
8
8
|
const visitState = await getInspectorState();
|
|
9
|
-
const { channelsWiringFile, channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings, schema, } = config;
|
|
10
|
-
const { channels } = visitState;
|
|
11
|
-
|
|
9
|
+
const { channelsWiringFile, channelsWiringMetaFile, channelsWiringMetaJsonFile, channelContractsMetaJsonFile, packageMappings, schema, } = config;
|
|
10
|
+
const { channels, exportedContracts } = visitState;
|
|
11
|
+
const hasChannelContracts = Object.keys(exportedContracts.channel).length > 0;
|
|
12
|
+
if ((channels.files.size === 0 || Object.keys(channels.meta).length === 0) &&
|
|
13
|
+
!hasChannelContracts) {
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
if (channels.files.size > 0 && Object.keys(channels.meta).length > 0) {
|
|
17
|
+
await writeFileInDir(logger, channelsWiringFile, serializeFileImports('addChannel', channelsWiringFile, channels.files, packageMappings));
|
|
18
|
+
}
|
|
19
|
+
if (Object.keys(channels.meta).length > 0) {
|
|
20
|
+
await writeFileInDir(logger, channelsWiringMetaJsonFile, JSON.stringify(channels.meta, null, 2));
|
|
21
|
+
}
|
|
22
|
+
await writeFileInDir(logger, channelContractsMetaJsonFile, JSON.stringify(exportedContracts.channel, null, 2));
|
|
23
|
+
if (Object.keys(channels.meta).length > 0) {
|
|
24
|
+
const jsonImportPath = getFileImportRelativePath(channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings);
|
|
25
|
+
const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
|
|
26
|
+
const importStatement = supportsImportAttributes
|
|
27
|
+
? `import metaData from '${jsonImportPath}' with { type: 'json' }`
|
|
28
|
+
: `import metaData from '${jsonImportPath}'`;
|
|
29
|
+
await writeFileInDir(logger, channelsWiringMetaFile, `import { pikkuState } from '@pikku/core/internal'\nimport { ChannelsMeta } from '@pikku/core/channel'\n${importStatement}\npikkuState(null, 'channel', 'meta', metaData as ChannelsMeta)`);
|
|
30
|
+
}
|
|
22
31
|
},
|
|
23
32
|
middleware: [
|
|
24
33
|
logCommandInfoAndTime({
|
|
@@ -5,8 +5,8 @@ import { serializeTypedChannelsMap } from './serialize-typed-channel-map.js';
|
|
|
5
5
|
export const pikkuChannelsMap = pikkuSessionlessFunc({
|
|
6
6
|
func: async ({ logger, config, getInspectorState }) => {
|
|
7
7
|
const state = await getInspectorState();
|
|
8
|
-
const { channelsMapDeclarationFile, packageMappings } = config;
|
|
9
|
-
const content = serializeTypedChannelsMap(logger, channelsMapDeclarationFile, packageMappings, state.functions.typesMap, state.functions.meta, state.addonFunctions, state.channels.meta);
|
|
8
|
+
const { channelsMapDeclarationFile, packageMappings, rpcInternalMapDeclarationFile, } = config;
|
|
9
|
+
const content = serializeTypedChannelsMap(logger, channelsMapDeclarationFile, packageMappings, state.functions.typesMap, state.functions.meta, state.addonFunctions, state.channels.meta, rpcInternalMapDeclarationFile);
|
|
10
10
|
await writeFileInDir(logger, channelsMapDeclarationFile, content);
|
|
11
11
|
},
|
|
12
12
|
middleware: [
|
|
@@ -7,21 +7,35 @@ import { stripVerboseFields, hasVerboseFields, } from '../../../utils/strip-verb
|
|
|
7
7
|
export const pikkuCommandChannels = pikkuSessionlessFunc({
|
|
8
8
|
func: async ({ logger, config, getInspectorState }) => {
|
|
9
9
|
const visitState = await getInspectorState();
|
|
10
|
-
const { channelsWiringFile, channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings, schema, } = config;
|
|
11
|
-
const { channels } = visitState;
|
|
12
|
-
|
|
10
|
+
const { channelsWiringFile, channelsWiringMetaFile, channelsWiringMetaJsonFile, channelContractsMetaJsonFile, channelContractsMetaFile, packageMappings, schema, } = config;
|
|
11
|
+
const { channels, exportedContracts } = visitState;
|
|
12
|
+
const hasChannelContracts = Object.keys(exportedContracts.channel).length > 0;
|
|
13
|
+
if ((channels.files.size === 0 || Object.keys(channels.meta).length === 0) &&
|
|
14
|
+
!hasChannelContracts) {
|
|
13
15
|
return undefined;
|
|
14
16
|
}
|
|
17
|
+
// The bootstrap imports channelsWiringFile and channelsWiringMetaFile
|
|
18
|
+
// whenever this command reports channels as active (truthy return), so both
|
|
19
|
+
// must always be written once past the guard above — including the
|
|
20
|
+
// contracts-only case where there are no local channel source files
|
|
21
|
+
// (channels.files is empty). Skipping either leaves the bootstrap importing
|
|
22
|
+
// a file that was never generated and the per-unit deploy bundle fails.
|
|
15
23
|
await writeFileInDir(logger, channelsWiringFile, serializeFileImports('addChannel', channelsWiringFile, channels.files, packageMappings));
|
|
16
|
-
// Write minimal JSON (runtime-only fields)
|
|
17
24
|
const minimalMeta = stripVerboseFields(channels.meta);
|
|
18
25
|
await writeFileInDir(logger, channelsWiringMetaJsonFile, JSON.stringify(minimalMeta, null, 2));
|
|
19
|
-
// Write verbose JSON only if it has additional fields
|
|
20
26
|
if (hasVerboseFields(channels.meta)) {
|
|
21
27
|
const verbosePath = channelsWiringMetaJsonFile.replace(/\.gen\.json$/, '-verbose.gen.json');
|
|
22
28
|
await writeFileInDir(logger, verbosePath, JSON.stringify(channels.meta, null, 2));
|
|
23
29
|
}
|
|
24
|
-
|
|
30
|
+
await writeFileInDir(logger, channelContractsMetaJsonFile, JSON.stringify(exportedContracts.channel, null, 2));
|
|
31
|
+
if (hasChannelContracts) {
|
|
32
|
+
const contractsJsonImportPath = getFileImportRelativePath(channelContractsMetaFile, channelContractsMetaJsonFile, packageMappings);
|
|
33
|
+
const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
|
|
34
|
+
const contractsImportStatement = supportsImportAttributes
|
|
35
|
+
? `import contractsMeta from '${contractsJsonImportPath}' with { type: 'json' }`
|
|
36
|
+
: `import contractsMeta from '${contractsJsonImportPath}'`;
|
|
37
|
+
await writeFileInDir(logger, channelContractsMetaFile, `${contractsImportStatement}\nexport default contractsMeta`);
|
|
38
|
+
}
|
|
25
39
|
const jsonImportPath = getFileImportRelativePath(channelsWiringMetaFile, channelsWiringMetaJsonFile, packageMappings);
|
|
26
40
|
const supportsImportAttributes = schema?.supportsImportAttributes ?? false;
|
|
27
41
|
const importStatement = supportsImportAttributes
|
|
@@ -2,4 +2,4 @@ import type { ChannelsMeta } from '@pikku/core/channel';
|
|
|
2
2
|
import type { TypesMap } from '@pikku/inspector';
|
|
3
3
|
import type { FunctionsMeta } from '@pikku/core';
|
|
4
4
|
import type { Logger } from '@pikku/core/services';
|
|
5
|
-
export declare const serializeTypedChannelsMap: (logger: Logger, relativeToPath: string, packageMappings: Record<string, string>, typesMap: TypesMap, functionsMeta: FunctionsMeta, addonFunctions: Record<string, FunctionsMeta>, channelsMeta: ChannelsMeta) => string;
|
|
5
|
+
export declare const serializeTypedChannelsMap: (logger: Logger, relativeToPath: string, packageMappings: Record<string, string>, typesMap: TypesMap, functionsMeta: FunctionsMeta, addonFunctions: Record<string, FunctionsMeta>, channelsMeta: ChannelsMeta, rpcInternalMapDeclarationFile: string) => string;
|