@pikku/cli 0.12.45 → 0.12.47

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.
Files changed (72) hide show
  1. package/console-app/assets/index-Cb-SEeMM.js +254 -0
  2. package/console-app/index.html +1 -1
  3. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  4. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  6. package/dist/.pikku/cli/pikku-cli-channel.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
  8. package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
  9. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  10. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  11. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  14. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  17. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  18. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  19. package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
  20. package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
  21. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  22. package/dist/.pikku/function/pikku-functions-meta.gen.json +124 -124
  23. package/dist/.pikku/function/pikku-functions.gen.js +1 -1
  24. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  25. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  26. package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
  27. package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
  28. package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  31. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  33. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  35. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  37. package/dist/.pikku/pikku-types.gen.js +1 -1
  38. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  39. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  41. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  43. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  44. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  45. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  46. package/dist/.pikku/schemas/register.gen.js +5 -5
  47. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
  48. package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
  49. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  50. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  51. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  52. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  53. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
  54. package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
  55. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  56. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  57. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
  58. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
  59. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  60. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  61. package/dist/bin/pikku-bin.mjs +2 -2
  62. package/dist/src/deploy/build-pipeline.js +2 -0
  63. package/dist/src/deploy/bundler/bundler.d.ts +1 -0
  64. package/dist/src/deploy/bundler/bundler.js +36 -5
  65. package/dist/src/deploy/provider-adapter.d.ts +7 -0
  66. package/dist/src/fabric/functions/validate.function.js +185 -0
  67. package/dist/src/functions/commands/tests-init.js +54 -8
  68. package/dist/src/functions/wirings/auth/pikku-command-auth.js +10 -1
  69. package/dist/src/scaffold/rpc-remote.gen.js +1 -1
  70. package/package.json +6 -6
  71. package/skills/pikku-better-auth/SKILL.md +19 -3
  72. package/console-app/assets/index-CRLT8CXr.js +0 -254
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  /* The files with an addQueueWorkers function call */
5
5
  import '../../src/scaffold/rpc-remote.gen.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { pikkuState } from '@pikku/core/internal';
5
5
  import metaData from './pikku-rpc-wirings-meta.internal.gen.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  /**
5
5
  * Scheduler-specific type definitions for tree-shaking optimization
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  /**
5
5
  * Scheduler-specific type definitions for tree-shaking optimization
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { addSchema } from '@pikku/core/schema';
5
5
  import * as PikkuSchemasOutput from './schemas/PikkuSchemasOutput.schema.json' with { type: 'json' };
@@ -216,8 +216,6 @@ import * as PikkuFunctionTypesInput from './schemas/PikkuFunctionTypesInput.sche
216
216
  addSchema('PikkuFunctionTypesInput', PikkuFunctionTypesInput);
217
217
  import * as PikkuFunctionsOutput from './schemas/PikkuFunctionsOutput.schema.json' with { type: 'json' };
218
218
  addSchema('PikkuFunctionsOutput', PikkuFunctionsOutput);
219
- import * as PikkuGatewayOutput from './schemas/PikkuGatewayOutput.schema.json' with { type: 'json' };
220
- addSchema('PikkuGatewayOutput', PikkuGatewayOutput);
221
219
  import * as PikkuCommandHTTPOutput from './schemas/PikkuCommandHTTPOutput.schema.json' with { type: 'json' };
222
220
  addSchema('PikkuCommandHTTPOutput', PikkuCommandHTTPOutput);
223
221
  import * as PikkuHTTPOutput from './schemas/PikkuHTTPOutput.schema.json' with { type: 'json' };
@@ -234,16 +232,18 @@ import * as PikkuCommandQueueOutput from './schemas/PikkuCommandQueueOutput.sche
234
232
  addSchema('PikkuCommandQueueOutput', PikkuCommandQueueOutput);
235
233
  import * as PikkuQueueOutput from './schemas/PikkuQueueOutput.schema.json' with { type: 'json' };
236
234
  addSchema('PikkuQueueOutput', PikkuQueueOutput);
235
+ import * as PikkuGatewayOutput from './schemas/PikkuGatewayOutput.schema.json' with { type: 'json' };
236
+ addSchema('PikkuGatewayOutput', PikkuGatewayOutput);
237
237
  import * as PikkuEventsScaffoldOutput from './schemas/PikkuEventsScaffoldOutput.schema.json' with { type: 'json' };
238
238
  addSchema('PikkuEventsScaffoldOutput', PikkuEventsScaffoldOutput);
239
- import * as PikkuSchedulerOutput from './schemas/PikkuSchedulerOutput.schema.json' with { type: 'json' };
240
- addSchema('PikkuSchedulerOutput', PikkuSchedulerOutput);
241
239
  import * as PikkuPublicRPCOutput from './schemas/PikkuPublicRPCOutput.schema.json' with { type: 'json' };
242
240
  addSchema('PikkuPublicRPCOutput', PikkuPublicRPCOutput);
243
241
  import * as PikkuRemoteRPCOutput from './schemas/PikkuRemoteRPCOutput.schema.json' with { type: 'json' };
244
242
  addSchema('PikkuRemoteRPCOutput', PikkuRemoteRPCOutput);
245
243
  import * as PikkuRPCOutput from './schemas/PikkuRPCOutput.schema.json' with { type: 'json' };
246
244
  addSchema('PikkuRPCOutput', PikkuRPCOutput);
245
+ import * as PikkuSchedulerOutput from './schemas/PikkuSchedulerOutput.schema.json' with { type: 'json' };
246
+ addSchema('PikkuSchedulerOutput', PikkuSchedulerOutput);
247
247
  import * as PikkuTriggerTypesInput from './schemas/PikkuTriggerTypesInput.schema.json' with { type: 'json' };
248
248
  addSchema('PikkuTriggerTypesInput', PikkuTriggerTypesInput);
249
249
  import * as PikkuTriggerOutput from './schemas/PikkuTriggerOutput.schema.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  export { wireSecret } from '@pikku/core/secret';
5
5
  export type { CoreSecret, SecretDefinitionMeta, SecretDefinitionsMeta } from '@pikku/core/secret';
@@ -1,4 +1,4 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  export { wireSecret } from '@pikku/core/secret';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { TypedSecretService as CoreTypedSecretService } from '@pikku/core/services';
5
5
  import type { SecretService } from '@pikku/core/services';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { TypedSecretService as CoreTypedSecretService } from '@pikku/core/services';
5
5
  const CREDENTIALS_META = {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  /**
5
5
  * Trigger-specific type definitions for tree-shaking optimization
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  /**
5
5
  * Trigger-specific type definitions for tree-shaking optimization
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  export { wireVariable } from '@pikku/core/variable';
5
5
  export type { CoreVariable, VariableDefinitionMeta, VariableDefinitionsMeta } from '@pikku/core/variable';
@@ -1,4 +1,4 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  export { wireVariable } from '@pikku/core/variable';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { TypedVariablesService as CoreTypedVariablesService } from '@pikku/core/services';
5
5
  import type { VariablesService } from '@pikku/core/services';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { TypedVariablesService as CoreTypedVariablesService } from '@pikku/core/services';
5
5
  const VARIABLES_META = {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { WorkflowCancelledException } from '@pikku/core/workflow';
5
5
  import { template } from '@pikku/core/workflow';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { WorkflowCancelledException } from '@pikku/core/workflow';
5
5
  import { template } from '@pikku/core/workflow';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { pikkuState } from '@pikku/core/internal';
5
5
  import allWorkflowMeta from './meta/allWorkflow.gen.json' with { type: 'json' };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * This file was generated by @pikku/cli@0.12.45
2
+ * This file was generated by @pikku/cli@0.12.47
3
3
  */
4
4
  import { addWorkflow } from '@pikku/core/workflow';
5
5
  import './pikku-workflow-wirings-meta.gen.js';
@@ -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.45') {
15
- process.stderr.write(`\n Update available 0.12.45 → ${latest}\n brew upgrade pikku or npm install -g @pikku/cli\n\n`)
14
+ if (latest !== '0.12.47') {
15
+ process.stderr.write(`\n Update available 0.12.47 → ${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 && SERVICE_GEN_FILE_MAP[kv[1]]) {
38
- patterns.push(SERVICE_GEN_FILE_MAP[kv[1]]);
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
- minify: false,
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 ──────────────────────────────
@@ -75,6 +75,33 @@ registerHooks({ Before, After, BeforeAll, AfterAll, setDefaultTimeout }, db)
75
75
  registerCommonSteps({ Given, When, Then })
76
76
  `;
77
77
  }
78
+ function starterFeature() {
79
+ return `Feature: Example function test
80
+
81
+ Starter scenario created by \`pikku tests init\`. It uses only the built-in
82
+ pikku/cucumber steps (no custom step code) and passes out of the box, so the
83
+ Run-tests button and coverage report work immediately. Replace it with real
84
+ scenarios that call your RPCs — see the commented example at the bottom.
85
+
86
+ Scenario: the function-test harness is wired up
87
+ Given the data "example" is:
88
+ | hello | world |
89
+ Then the data "example" is not empty
90
+
91
+ # Real example — call one of your RPCs and assert the outcome. Uncomment and
92
+ # adapt (run \`pikku meta\` to list versioned RPC names and input schemas):
93
+ #
94
+ # Scenario: an anonymous user cannot reach a protected RPC
95
+ # When an anonymous user calls "yourProtectedRpc"
96
+ # Then the call fails with "Unauthorized"
97
+ #
98
+ # Scenario: a public RPC returns data
99
+ # When an anonymous user calls "yourPublicRpc" with:
100
+ # | someField | someValue |
101
+ # Then the call succeeds
102
+ # And the result has "id"
103
+ `;
104
+ }
78
105
  function worldTs() {
79
106
  return `import { World, setWorldConstructor } from '@cucumber/cucumber'
80
107
  import { createFunctionWorld } from '@pikku/cucumber'
@@ -83,7 +110,7 @@ import { createStubServices } from './services.js'
83
110
  createFunctionWorld(World, setWorldConstructor, createStubServices)
84
111
  `;
85
112
  }
86
- function servicesTs(configImport, servicesImport, schemaImport, coercionImport, configVar, servicesVar, repoRootRel, hasDb) {
113
+ function servicesTs(configImport, servicesImport, schemaImport, coercionImport, configVar, servicesVar, repoRootRel, hasDb, migrationsRel, seedRel) {
87
114
  if (!hasDb) {
88
115
  return `import { createDbUtils, type StubTracker } from '@pikku/cucumber'
89
116
  import { ${configVar} } from '${configImport}'
@@ -114,8 +141,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
114
141
  const repoRoot = (p: string) => resolve(__dirname, '${repoRootRel}', p)
115
142
 
116
143
  export const db = createDbUtils({
117
- migrationsDir: repoRoot('db/migrations'),
118
- seedFile: repoRoot('db/seed.sql'),
144
+ migrationsDir: repoRoot('${migrationsRel}'),
145
+ seedFile: repoRoot('${seedRel}'),
119
146
  })
120
147
 
121
148
  type StubKysely = ReturnType<typeof createNodeSqliteKysely<DB>>
@@ -198,9 +225,28 @@ export const pikkuTestsInit = pikkuSessionlessFunc({
198
225
  const schemaImport = toJs(rel(schemaFile));
199
226
  const coercionImport = toJs(rel(coercionFile));
200
227
  const repoRootRel = relative(supportDir, config.rootDir);
201
- const hasDb = existsSync(join(config.rootDir, 'db', 'migrations'));
228
+ // Engine-aware db layout: stages live under db/sqlite or db/postgres with a
229
+ // matching db/<engine>-seed.sql (see the deploy convention). Pick whichever
230
+ // engine the project actually ships migrations for.
231
+ const engine = existsSync(join(config.rootDir, 'db', 'sqlite'))
232
+ ? 'sqlite'
233
+ : existsSync(join(config.rootDir, 'db', 'postgres'))
234
+ ? 'postgres'
235
+ : null;
236
+ // Only sqlite runs in the harness today (node:sqlite); postgres falls back
237
+ // to stubbed services until a pglite-backed harness lands.
238
+ const hasDb = engine === 'sqlite';
239
+ const migrationsRel = engine ? `db/${engine}` : '';
240
+ const seedRel = engine ? `db/${engine}-seed.sql` : '';
241
+ if (engine === 'postgres') {
242
+ logger.info('Note: Postgres function-test harness support is not wired yet (the harness runs on node:sqlite). Tracking in pikkujs/pikku#758 — the scaffold falls back to stubbed services.');
243
+ }
202
244
  const files = [
203
245
  [join(ftestDir, '.env.test'), envTest()],
246
+ // Empty lockfile so Yarn Berry treats tests/ as a standalone project
247
+ // rather than expecting it in the parent repo's workspaces (the harness
248
+ // is intentionally outside the workspace graph).
249
+ [join(ftestDir, 'yarn.lock'), ''],
204
250
  [join(ftestDir, 'package.json'), packageJson()],
205
251
  [join(ftestDir, 'tsconfig.json'), tsconfig()],
206
252
  [join(ftestDir, 'tests', 'cucumber.mjs'), cucumberMjs()],
@@ -208,9 +254,9 @@ export const pikkuTestsInit = pikkuSessionlessFunc({
208
254
  [join(supportDir, 'world.ts'), worldTs()],
209
255
  [
210
256
  join(supportDir, 'services.ts'),
211
- servicesTs(configImport, servicesImport, schemaImport, coercionImport, pikkuConfigFactory.variable, singletonServicesFactory.variable, repoRootRel, hasDb),
257
+ servicesTs(configImport, servicesImport, schemaImport, coercionImport, pikkuConfigFactory.variable, singletonServicesFactory.variable, repoRootRel, hasDb, migrationsRel, seedRel),
212
258
  ],
213
- [join(ftestDir, 'tests', 'features', '.gitkeep'), ''],
259
+ [join(ftestDir, 'tests', 'features', 'example.feature'), starterFeature()],
214
260
  ];
215
261
  for (const [filePath, content] of files) {
216
262
  await mkdir(dirname(filePath), { recursive: true });
@@ -219,7 +265,7 @@ export const pikkuTestsInit = pikkuSessionlessFunc({
219
265
  }
220
266
  logger.info('\nFunction test harness initialized.');
221
267
  logger.info('Next steps:');
222
- logger.info(' 1. Add your first .feature file under tests/features/');
223
- logger.info(' 2. Install @pikku/cucumber and run: yarn test');
268
+ logger.info(' 1. Install deps in tests/ and run: yarn test');
269
+ logger.info(' 2. Edit tests/tests/features/example.feature add scenarios that call your RPCs');
224
270
  },
225
271
  });