@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.
- package/console-app/assets/index-Cb-SEeMM.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 +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- 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 +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +124 -124
- 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.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/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 +5 -5
- 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/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/commands/tests-init.js +54 -8
- package/dist/src/functions/wirings/auth/pikku-command-auth.js +10 -1
- package/dist/src/scaffold/rpc-remote.gen.js +1 -1
- package/package.json +6 -6
- package/skills/pikku-better-auth/SKILL.md +19 -3
- 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.
|
|
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.
|
|
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';
|
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.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
|
|
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 ──────────────────────────────
|
|
@@ -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('
|
|
118
|
-
seedFile: repoRoot('
|
|
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
|
-
|
|
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', '.
|
|
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.
|
|
223
|
-
logger.info(' 2.
|
|
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
|
});
|