@pikku/cli 0.12.45 → 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/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 +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 +110 -110
- 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/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 +3 -3
- 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/wirings/auth/pikku-command-auth.js +10 -1
- package/dist/src/scaffold/rpc-remote.gen.js +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
|
@@ -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.46
|
|
3
3
|
*/
|
|
4
4
|
import { addSchema } from '@pikku/core/schema';
|
|
5
5
|
import * as PikkuSchemasOutput from './schemas/PikkuSchemasOutput.schema.json' with { type: 'json' };
|
|
@@ -236,14 +236,14 @@ import * as PikkuQueueOutput from './schemas/PikkuQueueOutput.schema.json' with
|
|
|
236
236
|
addSchema('PikkuQueueOutput', PikkuQueueOutput);
|
|
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.46
|
|
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.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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.46",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"imports": {
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
"@openapi-contrib/json-schema-to-openapi-schema": "^4.3.1",
|
|
30
30
|
"@pikku/better-auth": "^0.12.9",
|
|
31
31
|
"@pikku/core": "^0.12.35",
|
|
32
|
-
"@pikku/deploy-cloudflare": "^0.12.
|
|
32
|
+
"@pikku/deploy-cloudflare": "^0.12.4",
|
|
33
33
|
"@pikku/fetch": "^0.12.3",
|
|
34
|
-
"@pikku/inspector": "^0.12.
|
|
34
|
+
"@pikku/inspector": "^0.12.24",
|
|
35
35
|
"@pikku/kysely": "^0.12.16",
|
|
36
36
|
"@pikku/kysely-node-sqlite": "^0.12.2",
|
|
37
37
|
"@pikku/node-http-server": "^0.12.2",
|
|
@@ -46,9 +46,9 @@ yarn add @pikku/better-auth better-auth
|
|
|
46
46
|
Better Auth owns its own HTTP surface, database tables, and session cookie. The Pikku integration is thin:
|
|
47
47
|
|
|
48
48
|
1. **`pikkuBetterAuth(factory)`** — you export ONE `pikkuBetterAuth` call whose factory returns a configured `betterAuth({...})` instance. The pikku CLI inspects this export and generates everything else.
|
|
49
|
-
2. **Generated `auth.gen.ts`** — a catch-all `${basePath}{/*splat}` HTTP route per method (GET + POST) that forwards every request under the base path to better-auth's own internal router
|
|
50
|
-
3. **Generated `auth-
|
|
51
|
-
4.
|
|
49
|
+
2. **Generated `auth.gen.ts`** — a catch-all `${basePath}{/*splat}` HTTP route per method (GET + POST) that forwards every request under the base path to better-auth's own internal router. The enabled providers and plugins are written to `auth/pikku-auth-meta.gen.json` (read by the console SSO page via `getAuthProviders`).
|
|
50
|
+
3. **Generated session middleware** — with `session.cookieCache` enabled (recommended), a separate `auth-middleware.gen.ts` adds the lean stateless `betterAuthStatelessSession()`; without it, `auth.gen.ts` adds the stateful `betterAuthSession()` that bundles the full server into every unit. See "Stateless session" below.
|
|
51
|
+
4. **Generated `auth-secrets.gen.ts`** — a `wireSecret` for `BETTER_AUTH_SECRET` and for each social provider's OAuth credentials, plus a `wireVariable` for any non-secret provider config (e.g. `tenantId`).
|
|
52
52
|
|
|
53
53
|
You do NOT hand-write routes, the session middleware, or the secret wiring — `pikkuBetterAuth` + the CLI generate all of it. Re-run `pikku auth` (or `pikku all`) to regenerate.
|
|
54
54
|
|
|
@@ -78,6 +78,8 @@ export const auth = pikkuBetterAuth(async ({ secrets }) => {
|
|
|
78
78
|
// at runtime. Swap for the Kysely adapter in production (see below).
|
|
79
79
|
database: memoryAdapter({ user: [], session: [], account: [], verification: [] }),
|
|
80
80
|
emailAndPassword: { enabled: true },
|
|
81
|
+
// ALWAYS enable for deployed apps — see "Stateless session" below.
|
|
82
|
+
session: { cookieCache: { enabled: true } },
|
|
81
83
|
socialProviders: {
|
|
82
84
|
github: GITHUB_OAUTH,
|
|
83
85
|
},
|
|
@@ -89,6 +91,19 @@ export const auth = pikkuBetterAuth(async ({ secrets }) => {
|
|
|
89
91
|
- `socialProviders` keys must be string literals — the CLI reads them statically to emit a `wireSecret` per provider. Provider keys mirror better-auth's built-in ids exactly (e.g. `microsoft`, NOT `microsoft-entra-id`; `cognito`; `github`).
|
|
90
92
|
- The factory runs lazily on the first auth request, so it pulls secrets/DB off the injected `services`.
|
|
91
93
|
- The default `basePath` is `/api/auth`. Override it by passing `basePath` to `betterAuth`.
|
|
94
|
+
- **Enable `session: { cookieCache: { enabled: true } }`** so non-auth units tree-shake the better-auth server out (see below).
|
|
95
|
+
|
|
96
|
+
## ⚠️ Stateless session — ALWAYS enable `cookieCache` for deployed apps
|
|
97
|
+
|
|
98
|
+
By default the CLI wires the **stateful** `betterAuthSession` bridge globally — it calls `services.auth()`, so EVERY unit/worker bundles the full better-auth server (~2.5MB each). On per-unit deploy targets (Fabric/Cloudflare) that bloats every bundle and the serial upload phase.
|
|
99
|
+
|
|
100
|
+
Enabling `session: { cookieCache: { enabled: true } }` makes the CLI split out a lean `betterAuthStatelessSession` (`src/scaffold/auth-middleware.gen.ts`) that verifies the signed session cookie using only `BETTER_AUTH_SECRET` — no `services.auth()`, no server bundled. Non-auth units drop from ~2.5MB to ~20KB. Only the auth unit carries the server. `pikku fabric validate` warns (`better-auth-stateless-session-disabled`) when it's off.
|
|
101
|
+
|
|
102
|
+
**Tradeoff:** server-side session revocation isn't seen until the cookie cache expires (sign-out is still immediate — it deletes the cookie).
|
|
103
|
+
|
|
104
|
+
**Do NOT also hand-write a global `addHTTPMiddleware('*', [betterAuthSession()])`** — that re-drags the stateful server into every unit and defeats the split (validate flags it as `better-auth-stateful-session-global`). The generated middleware is enough.
|
|
105
|
+
|
|
106
|
+
**Custom session fields (`role`, `locale`, …):** the generated stateless middleware uses the default map (`{ userId }` only). To use a custom map, register your own `betterAuthStatelessSession({ mapSession })` **globally** — `addHTTPMiddleware('*', [...])` or `addGlobalMiddleware([...])`. The CLI detects a global user registration and skips generating its own (pikkujs/pikku#754), so you keep cookieCache's lean bundles *and* your custom fields. A route-scoped registration (`addHTTPMiddleware('/some/path', [...])`) does not count — the generated global middleware is still emitted.
|
|
92
107
|
|
|
93
108
|
### 2. Production database adapter
|
|
94
109
|
|
|
@@ -105,6 +120,7 @@ export const auth = pikkuBetterAuth(async ({ secrets, kysely }) => {
|
|
|
105
120
|
secret: BETTER_AUTH_SECRET,
|
|
106
121
|
database: kyselyAdapter(kysely, { type: 'postgres' }),
|
|
107
122
|
emailAndPassword: { enabled: true },
|
|
123
|
+
session: { cookieCache: { enabled: true } },
|
|
108
124
|
})
|
|
109
125
|
})
|
|
110
126
|
```
|