@pikku/deploy-standalone 0.12.12 → 0.12.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/adapter.d.ts +34 -0
- package/dist/adapter.js +184 -4
- package/dist/runtime/index.d.ts +9 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/parent-watch.d.ts +45 -0
- package/dist/runtime/parent-watch.js +87 -0
- package/dist/tauri/generate.d.ts +45 -0
- package/dist/tauri/generate.js +230 -0
- package/dist/tauri/icon.d.ts +1 -0
- package/dist/tauri/icon.js +54 -0
- package/dist/tauri/main-rs.d.ts +31 -0
- package/dist/tauri/main-rs.js +213 -0
- package/dist/tauri/next-steps.d.ts +15 -0
- package/dist/tauri/next-steps.js +16 -0
- package/dist/tauri/target-triple.d.ts +29 -0
- package/dist/tauri/target-triple.js +42 -0
- package/knowledge/decisions/a-pikku-server-serves-a-static-frontend.md +36 -0
- package/knowledge/decisions/a-remote-desktop-shell-bundles-nothing.md +38 -0
- package/knowledge/decisions/deploy-consumes-a-built-frontend.md +33 -0
- package/knowledge/decisions/desktop-builds-are-unsigned-and-never-update-themselves.md +34 -0
- package/knowledge/decisions/index.md +19 -0
- package/knowledge/decisions/standalone-assets-are-embedded-in-the-bun-binary.md +39 -0
- package/knowledge/decisions/the-desktop-shell-runs-the-server-as-a-sidecar.md +51 -0
- package/knowledge/decisions/the-sidecar-reports-its-port-the-shell-never-picks-one.md +44 -0
- package/knowledge/index.md +22 -0
- package/package.json +6 -4
- package/src/adapter.test.ts +186 -0
- package/src/adapter.ts +210 -4
- package/src/desktop-deploy.test.ts +167 -0
- package/src/runtime/index.ts +13 -0
- package/src/runtime/parent-watch.process.test.ts +112 -0
- package/src/runtime/parent-watch.test.ts +148 -0
- package/src/runtime/parent-watch.ts +115 -0
- package/src/sidecar-entry.test.ts +89 -0
- package/src/tauri/generate.test.ts +401 -0
- package/src/tauri/generate.ts +327 -0
- package/src/tauri/icon.test.ts +63 -0
- package/src/tauri/icon.ts +62 -0
- package/src/tauri/main-rs.rustfmt.test.ts +86 -0
- package/src/tauri/main-rs.ts +241 -0
- package/src/tauri/next-steps.test.ts +38 -0
- package/src/tauri/next-steps.ts +30 -0
- package/src/tauri/target-triple.test.ts +84 -0
- package/src/tauri/target-triple.ts +65 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# @pikku/deploy-standalone
|
|
2
2
|
|
|
3
|
+
## 0.12.13
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 80eb5c0: Generate a desktop shell from `pikku deploy apply --desktop`
|
|
8
|
+
|
|
9
|
+
`pikku deploy apply --provider standalone --runtime bun --desktop` now emits a
|
|
10
|
+
`src-tauri/` crate that ships the compiled binary as a sidecar and opens a
|
|
11
|
+
window on the server's own HTTP origin, so cookies, CORS and OAuth behave
|
|
12
|
+
exactly as they do in a browser. Regeneration is idempotent and leaves an
|
|
13
|
+
edited file alone rather than overwriting it.
|
|
14
|
+
|
|
15
|
+
`--desktop-url https://app.example.com` builds the other shape: a shell that
|
|
16
|
+
points at an already-deployed server. Nothing is bundled — no sidecar, no
|
|
17
|
+
binary, and so no bun runtime to compile one — and the window is declared in
|
|
18
|
+
`tauri.conf.json` rather than opened from Rust, because the origin is known up
|
|
19
|
+
front. The url can also live in `pikku.config.json` as `deploy.desktop.url`,
|
|
20
|
+
alongside `deploy.desktop.identifier`.
|
|
21
|
+
|
|
22
|
+
Supporting changes: `SERVER_READY_MARKER` moved to `@pikku/deploy` (the CLI
|
|
23
|
+
re-exports it from its old path), both HTTP runtimes expose the port they
|
|
24
|
+
actually bound so `--port 0` reports a real port, and the generated server
|
|
25
|
+
entry exits when its parent process goes away.
|
|
26
|
+
|
|
27
|
+
- 80eb5c0: feat: serve a built frontend from the pikku server's own origin
|
|
28
|
+
|
|
29
|
+
A new `frontend` key in `pikku.config.json` names a directory of built
|
|
30
|
+
frontend output. `pikku serve` mounts it, and `pikku deploy` ships it inside
|
|
31
|
+
the distributable — into a directory beside the bundle for the node runtime,
|
|
32
|
+
and embedded in the binary for a `bun build --compile` standalone. `pikku dev`
|
|
33
|
+
deliberately ignores it and says so, because the frontend's own dev server owns
|
|
34
|
+
that job.
|
|
35
|
+
|
|
36
|
+
Pikku reads the frontend's output and never builds it, so an unbuilt directory
|
|
37
|
+
fails with a message that says which build to run rather than booting a server
|
|
38
|
+
that answers every page with a 404.
|
|
39
|
+
|
|
40
|
+
- Updated dependencies [80eb5c0]
|
|
41
|
+
- Updated dependencies [80eb5c0]
|
|
42
|
+
- @pikku/deploy@0.12.2
|
|
43
|
+
|
|
3
44
|
## 0.12.12
|
|
4
45
|
|
|
5
46
|
### Patch Changes
|
package/dist/adapter.d.ts
CHANGED
|
@@ -18,14 +18,46 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { EntryGenerationContext, ProviderAdapter } from '@pikku/deploy';
|
|
20
20
|
export type StandaloneRuntime = 'node' | 'bun';
|
|
21
|
+
/**
|
|
22
|
+
* Directory the built frontend is copied to, both inside the unit and beside
|
|
23
|
+
* the shipped bundle. The node entry resolves it relative to itself at runtime,
|
|
24
|
+
* so the two have to agree.
|
|
25
|
+
*/
|
|
26
|
+
export declare const STANDALONE_FRONTEND_DIR = "frontend";
|
|
27
|
+
/**
|
|
28
|
+
* Module the bun entry imports its embedded assets from. It stays out of the
|
|
29
|
+
* esbuild bundle — esbuild rejects the `with { type: 'file' }` attribute the
|
|
30
|
+
* manifest is built on — and is resolved by `bun build --compile` instead.
|
|
31
|
+
*/
|
|
32
|
+
export declare const STANDALONE_FRONTEND_MANIFEST = "./frontend-assets.gen.js";
|
|
21
33
|
export interface StandaloneProviderAdapterOptions {
|
|
22
34
|
runtime?: StandaloneRuntime;
|
|
35
|
+
/**
|
|
36
|
+
* Generate a desktop shell (Tauri) around the compiled binary. Requires the
|
|
37
|
+
* `bun` runtime — the shell ships the binary as a sidecar, and only that
|
|
38
|
+
* runtime produces one. A shell pointed at {@link desktopUrl} ships no binary
|
|
39
|
+
* and so has no such requirement.
|
|
40
|
+
*/
|
|
41
|
+
desktop?: boolean;
|
|
42
|
+
/** Project root. The shell crate is written to `<projectDir>/src-tauri`. */
|
|
43
|
+
projectDir?: string;
|
|
44
|
+
/** Bundle identifier for the shell. Derived from the app name when absent. */
|
|
45
|
+
desktopIdentifier?: string;
|
|
46
|
+
/**
|
|
47
|
+
* An already-deployed server for the shell to open, instead of bundling one.
|
|
48
|
+
* The window is a webview onto that origin and nothing else is shipped.
|
|
49
|
+
*/
|
|
50
|
+
desktopUrl?: string;
|
|
23
51
|
}
|
|
24
52
|
export declare class StandaloneProviderAdapter implements ProviderAdapter {
|
|
25
53
|
readonly name = "standalone";
|
|
26
54
|
readonly deployDirName = "standalone";
|
|
27
55
|
readonly singleUnit = true;
|
|
28
56
|
readonly runtime: StandaloneRuntime;
|
|
57
|
+
readonly desktop: boolean;
|
|
58
|
+
readonly projectDir?: string;
|
|
59
|
+
readonly desktopIdentifier?: string;
|
|
60
|
+
readonly desktopUrl?: string;
|
|
29
61
|
constructor(options?: StandaloneProviderAdapterOptions);
|
|
30
62
|
generateEntrySource(ctx: EntryGenerationContext): string;
|
|
31
63
|
private generateNodeEntrySource;
|
|
@@ -50,10 +82,12 @@ export declare class StandaloneProviderAdapter implements ProviderAdapter {
|
|
|
50
82
|
}[];
|
|
51
83
|
workersDeployed?: undefined;
|
|
52
84
|
resourcesCreated?: undefined;
|
|
85
|
+
targetTriple?: undefined;
|
|
53
86
|
} | {
|
|
54
87
|
success: boolean;
|
|
55
88
|
workersDeployed: string[];
|
|
56
89
|
resourcesCreated: never[];
|
|
57
90
|
errors: never[];
|
|
91
|
+
targetTriple: string | undefined;
|
|
58
92
|
}>;
|
|
59
93
|
}
|
package/dist/adapter.js
CHANGED
|
@@ -1,11 +1,60 @@
|
|
|
1
|
-
import { nodeBuiltinExternals } from '@pikku/deploy';
|
|
1
|
+
import { nodeBuiltinExternals, SERVER_READY_MARKER } from '@pikku/deploy';
|
|
2
|
+
/**
|
|
3
|
+
* Directory the built frontend is copied to, both inside the unit and beside
|
|
4
|
+
* the shipped bundle. The node entry resolves it relative to itself at runtime,
|
|
5
|
+
* so the two have to agree.
|
|
6
|
+
*/
|
|
7
|
+
export const STANDALONE_FRONTEND_DIR = 'frontend';
|
|
8
|
+
/**
|
|
9
|
+
* Module the bun entry imports its embedded assets from. It stays out of the
|
|
10
|
+
* esbuild bundle — esbuild rejects the `with { type: 'file' }` attribute the
|
|
11
|
+
* manifest is built on — and is resolved by `bun build --compile` instead.
|
|
12
|
+
*/
|
|
13
|
+
export const STANDALONE_FRONTEND_MANIFEST = './frontend-assets.gen.js';
|
|
14
|
+
/**
|
|
15
|
+
* Lines every standalone entry ends with, whatever the runtime.
|
|
16
|
+
*
|
|
17
|
+
* The ready line is the handshake a parent process — `pikku dev --spawn`, or
|
|
18
|
+
* the desktop shell that runs this binary as a sidecar — blocks on. It carries
|
|
19
|
+
* `server.port` rather than the requested port because a shell passes `PORT=0`:
|
|
20
|
+
* picking a free port in the parent and handing it down races anything else
|
|
21
|
+
* that binds it in between, so the server binds first and reports back.
|
|
22
|
+
*/
|
|
23
|
+
const sidecarHandshakeLines = () => [
|
|
24
|
+
` watchParentProcess()`,
|
|
25
|
+
` console.log(\`${SERVER_READY_MARKER} on http://\${hostname}:\${server.port}\`)`,
|
|
26
|
+
];
|
|
27
|
+
const SIDECAR_RUNTIME_IMPORT = `import { watchParentProcess } from '@pikku/deploy-standalone/runtime'`;
|
|
28
|
+
/**
|
|
29
|
+
* `rustc -vV`, or nothing when no toolchain is installed. The triple then falls
|
|
30
|
+
* back to the Node platform pair, which is right for every ordinary host — the
|
|
31
|
+
* cases rustc knows better about (musl, Rosetta) are the ones where a Rust
|
|
32
|
+
* toolchain is present anyway.
|
|
33
|
+
*/
|
|
34
|
+
const rustcHostOutput = async () => {
|
|
35
|
+
try {
|
|
36
|
+
const { execFileSync } = await import('node:child_process');
|
|
37
|
+
return execFileSync('rustc', ['-vV'], { encoding: 'utf-8', stdio: 'pipe' });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
2
43
|
export class StandaloneProviderAdapter {
|
|
3
44
|
name = 'standalone';
|
|
4
45
|
deployDirName = 'standalone';
|
|
5
46
|
singleUnit = true;
|
|
6
47
|
runtime;
|
|
48
|
+
desktop;
|
|
49
|
+
projectDir;
|
|
50
|
+
desktopIdentifier;
|
|
51
|
+
desktopUrl;
|
|
7
52
|
constructor(options = {}) {
|
|
8
53
|
this.runtime = options.runtime ?? 'node';
|
|
54
|
+
this.desktop = options.desktop ?? Boolean(options.desktopUrl);
|
|
55
|
+
this.projectDir = options.projectDir;
|
|
56
|
+
this.desktopIdentifier = options.desktopIdentifier;
|
|
57
|
+
this.desktopUrl = options.desktopUrl;
|
|
9
58
|
}
|
|
10
59
|
generateEntrySource(ctx) {
|
|
11
60
|
if (this.runtime === 'bun') {
|
|
@@ -24,6 +73,13 @@ export class StandaloneProviderAdapter {
|
|
|
24
73
|
`import { PikkuNodeHTTPServer } from '@pikku/node-http-server'`,
|
|
25
74
|
`import { DEFAULT_WS_MAX_PAYLOAD, pikkuWebsocketHandler } from '@pikku/ws'`,
|
|
26
75
|
`import { WebSocketServer } from 'ws'`,
|
|
76
|
+
SIDECAR_RUNTIME_IMPORT,
|
|
77
|
+
...(ctx.frontend
|
|
78
|
+
? [
|
|
79
|
+
`import { dirname as __pikkuDirname, join as __pikkuJoin } from 'node:path'`,
|
|
80
|
+
`import { fileURLToPath as __pikkuFileURLToPath } from 'node:url'`,
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
27
83
|
``,
|
|
28
84
|
ctx.configImport,
|
|
29
85
|
ctx.servicesImport,
|
|
@@ -55,9 +111,21 @@ export class StandaloneProviderAdapter {
|
|
|
55
111
|
` })`,
|
|
56
112
|
` pikkuState(null, 'package', 'singletonServices', singletonServices)`,
|
|
57
113
|
``,
|
|
114
|
+
...(ctx.frontend
|
|
115
|
+
? [
|
|
116
|
+
// Resolved from the running bundle rather than baked in at build
|
|
117
|
+
// time, so the distributable stays movable.
|
|
118
|
+
` const staticMounts = [{`,
|
|
119
|
+
` urlPrefix: '${ctx.frontend.urlPrefix}',`,
|
|
120
|
+
` directory: __pikkuJoin(__pikkuDirname(__pikkuFileURLToPath(import.meta.url)), '${STANDALONE_FRONTEND_DIR}'),`,
|
|
121
|
+
` spaFallback: ${ctx.frontend.spaFallback},`,
|
|
122
|
+
` }]`,
|
|
123
|
+
``,
|
|
124
|
+
]
|
|
125
|
+
: []),
|
|
58
126
|
` const wss = new WebSocketServer({ noServer: true, maxPayload: DEFAULT_WS_MAX_PAYLOAD })`,
|
|
59
127
|
` const server = new PikkuNodeHTTPServer(`,
|
|
60
|
-
` { ...config, port, hostname },`,
|
|
128
|
+
` { ...config, port, hostname${ctx.frontend ? ', staticMounts' : ''} },`,
|
|
61
129
|
` logger,`,
|
|
62
130
|
` {`,
|
|
63
131
|
` ${ctx.mcpServerOption}configureServer: (httpServer) => {`,
|
|
@@ -70,6 +138,7 @@ export class StandaloneProviderAdapter {
|
|
|
70
138
|
` await triggerService.start()`,
|
|
71
139
|
` server.enableExitOnSignals()`,
|
|
72
140
|
` await server.start()`,
|
|
141
|
+
...sidecarHandshakeLines(),
|
|
73
142
|
`}`,
|
|
74
143
|
``,
|
|
75
144
|
`main().catch((err) => {`,
|
|
@@ -83,10 +152,14 @@ export class StandaloneProviderAdapter {
|
|
|
83
152
|
return [
|
|
84
153
|
`// Generated standalone entry (bun runtime) — all functions in one process`,
|
|
85
154
|
`import { ConsoleLogger, InMemoryQueueService, InMemoryTriggerService, InMemoryWorkflowService } from '@pikku/core/services'`,
|
|
155
|
+
SIDECAR_RUNTIME_IMPORT,
|
|
86
156
|
`import { pikkuState } from '@pikku/core/state'`,
|
|
87
157
|
`import { wireAgentScorerQueueWorkers } from '@pikku/core/agent-scorer'`,
|
|
88
158
|
`import { InMemorySchedulerService } from '@pikku/schedule'`,
|
|
89
159
|
`import { PikkuBunServer, BunEventHubService } from '@pikku/bun-server'`,
|
|
160
|
+
...(ctx.frontend
|
|
161
|
+
? [`import { frontendAssets } from '${STANDALONE_FRONTEND_MANIFEST}'`]
|
|
162
|
+
: []),
|
|
90
163
|
``,
|
|
91
164
|
ctx.configImport,
|
|
92
165
|
ctx.servicesImport,
|
|
@@ -118,12 +191,26 @@ export class StandaloneProviderAdapter {
|
|
|
118
191
|
` })`,
|
|
119
192
|
` pikkuState(null, 'package', 'singletonServices', singletonServices)`,
|
|
120
193
|
``,
|
|
121
|
-
|
|
194
|
+
...(ctx.frontend
|
|
195
|
+
? [
|
|
196
|
+
// A compiled binary has no directory to read: every file was
|
|
197
|
+
// embedded, and the map is the only way back to it.
|
|
198
|
+
` const staticMounts = [{`,
|
|
199
|
+
` urlPrefix: '${ctx.frontend.urlPrefix}',`,
|
|
200
|
+
` directory: '',`,
|
|
201
|
+
` spaFallback: ${ctx.frontend.spaFallback},`,
|
|
202
|
+
` assets: frontendAssets,`,
|
|
203
|
+
` }]`,
|
|
204
|
+
``,
|
|
205
|
+
]
|
|
206
|
+
: []),
|
|
207
|
+
` const server = new PikkuBunServer({ ...config, port, hostname${ctx.frontend ? ', staticMounts' : ''} }, logger, { ${ctx.mcpServerOption}eventHub })`,
|
|
122
208
|
` await server.init()`,
|
|
123
209
|
` await schedulerService.start()`,
|
|
124
210
|
` await triggerService.start()`,
|
|
125
211
|
` server.enableExitOnSignals()`,
|
|
126
212
|
` await server.start()`,
|
|
213
|
+
...sidecarHandshakeLines(),
|
|
127
214
|
`}`,
|
|
128
215
|
``,
|
|
129
216
|
`main().catch((err) => {`,
|
|
@@ -148,6 +235,7 @@ export class StandaloneProviderAdapter {
|
|
|
148
235
|
// Bun-native builtins are provided by the runtime and resolved by
|
|
149
236
|
// `bun build --compile` — leave them as imports rather than inlining.
|
|
150
237
|
externals.push('bun', 'bun:*', 'bun:sqlite', 'bun:ffi');
|
|
238
|
+
externals.push(STANDALONE_FRONTEND_MANIFEST);
|
|
151
239
|
}
|
|
152
240
|
return externals;
|
|
153
241
|
}
|
|
@@ -156,8 +244,34 @@ export class StandaloneProviderAdapter {
|
|
|
156
244
|
}
|
|
157
245
|
async deploy(options) {
|
|
158
246
|
const { buildDir, logger } = options;
|
|
247
|
+
// Checked before anything expensive runs: a `--desktop` deploy that cannot
|
|
248
|
+
// produce a shell should say so now, not after a bun compile.
|
|
249
|
+
if (this.desktop) {
|
|
250
|
+
if (!this.desktopUrl && this.runtime !== 'bun') {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
errors: [
|
|
254
|
+
{
|
|
255
|
+
step: 'desktop',
|
|
256
|
+
error: `A desktop shell ships the server as a sidecar binary, which only the bun runtime produces. Re-run with --runtime bun (got '${this.runtime}').`,
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (!this.projectDir) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
errors: [
|
|
265
|
+
{
|
|
266
|
+
step: 'desktop',
|
|
267
|
+
error: 'No project directory was supplied, so there is nowhere to write src-tauri/.',
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
159
273
|
const { join, dirname } = await import('node:path');
|
|
160
|
-
const { readdir, writeFile, copyFile, mkdir } = await import('node:fs/promises');
|
|
274
|
+
const { cp, readdir, writeFile, copyFile, mkdir } = await import('node:fs/promises');
|
|
161
275
|
const { existsSync } = await import('node:fs');
|
|
162
276
|
// Find the unit dir with the bundle
|
|
163
277
|
const entries = await readdir(buildDir);
|
|
@@ -179,6 +293,21 @@ export class StandaloneProviderAdapter {
|
|
|
179
293
|
await copyFile(join(unitDir, 'bundle.js.map'), join(outDir, 'bundle.js.map'));
|
|
180
294
|
}
|
|
181
295
|
logger.info(`Bundle: ${join(outDir, 'bundle.js')}`);
|
|
296
|
+
// --- 2a. Frontend, when the build produced one ---
|
|
297
|
+
// Both runtimes need it here rather than only in the build directory: node
|
|
298
|
+
// resolves the mount relative to the shipped bundle, and `bun build
|
|
299
|
+
// --compile` follows the manifest import out of the copy it is given.
|
|
300
|
+
const frontendDir = join(unitDir, STANDALONE_FRONTEND_DIR);
|
|
301
|
+
if (existsSync(frontendDir)) {
|
|
302
|
+
await cp(frontendDir, join(outDir, STANDALONE_FRONTEND_DIR), {
|
|
303
|
+
recursive: true,
|
|
304
|
+
});
|
|
305
|
+
const manifestName = STANDALONE_FRONTEND_MANIFEST.replace('./', '');
|
|
306
|
+
if (existsSync(join(unitDir, manifestName))) {
|
|
307
|
+
await copyFile(join(unitDir, manifestName), join(outDir, manifestName));
|
|
308
|
+
}
|
|
309
|
+
logger.info(`Frontend: ${join(outDir, STANDALONE_FRONTEND_DIR)}`);
|
|
310
|
+
}
|
|
182
311
|
// --- 2b. bun runtime: compile the bundle into a self-contained binary ---
|
|
183
312
|
if (this.runtime === 'bun') {
|
|
184
313
|
const { execFileSync } = await import('node:child_process');
|
|
@@ -206,6 +335,56 @@ export class StandaloneProviderAdapter {
|
|
|
206
335
|
};
|
|
207
336
|
}
|
|
208
337
|
}
|
|
338
|
+
// --- 2c. desktop: wrap the server in a shell, or point one at a remote ---
|
|
339
|
+
let targetTriple;
|
|
340
|
+
if (this.desktop && this.projectDir) {
|
|
341
|
+
const { generateTauriShell, tauriBundleIdentifier } = await import('./tauri/generate.js');
|
|
342
|
+
const { hostTargetTriple } = await import('./tauri/target-triple.js');
|
|
343
|
+
const { renderTauriNextSteps } = await import('./tauri/next-steps.js');
|
|
344
|
+
try {
|
|
345
|
+
const rustcVersionVerbose = await rustcHostOutput();
|
|
346
|
+
targetTriple = hostTargetTriple({ rustcVersionVerbose });
|
|
347
|
+
const shell = await generateTauriShell({
|
|
348
|
+
projectDir: this.projectDir,
|
|
349
|
+
appName,
|
|
350
|
+
identifier: this.desktopIdentifier ?? tauriBundleIdentifier(appName),
|
|
351
|
+
targetTriple,
|
|
352
|
+
...(this.desktopUrl
|
|
353
|
+
? { remoteUrl: this.desktopUrl }
|
|
354
|
+
: { binaryPath: join(outDir, appName) }),
|
|
355
|
+
});
|
|
356
|
+
logger.info(`Desktop shell: ${shell.dir} (${shell.targetTriple})`);
|
|
357
|
+
if (shell.written.length) {
|
|
358
|
+
logger.info(` wrote ${shell.written.join(', ')}`);
|
|
359
|
+
}
|
|
360
|
+
if (shell.preserved.length) {
|
|
361
|
+
logger.info(` kept your edits, not regenerated: ${shell.preserved.join(', ')}`);
|
|
362
|
+
}
|
|
363
|
+
if (shell.sidecar) {
|
|
364
|
+
logger.info(` sidecar: binaries/${shell.sidecar.fileName}`);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
logger.info(` window opens: ${this.desktopUrl}`);
|
|
368
|
+
}
|
|
369
|
+
for (const line of renderTauriNextSteps({
|
|
370
|
+
shellDir: shell.dir,
|
|
371
|
+
hasRust: rustcVersionVerbose !== undefined,
|
|
372
|
+
})) {
|
|
373
|
+
logger.info(line);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
errors: [
|
|
380
|
+
{
|
|
381
|
+
step: 'desktop',
|
|
382
|
+
error: e instanceof Error ? e.message : String(e),
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
209
388
|
// --- 3. config/ — empty template with .env example ---
|
|
210
389
|
const configDir = join(outDir, 'config');
|
|
211
390
|
await mkdir(configDir, { recursive: true });
|
|
@@ -228,6 +407,7 @@ export class StandaloneProviderAdapter {
|
|
|
228
407
|
workersDeployed: [appName],
|
|
229
408
|
resourcesCreated: [],
|
|
230
409
|
errors: [],
|
|
410
|
+
targetTriple,
|
|
231
411
|
};
|
|
232
412
|
}
|
|
233
413
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@pikku/deploy-standalone/runtime` — the sliver of this package that runs
|
|
3
|
+
* inside the shipped artifact rather than on the build machine.
|
|
4
|
+
*
|
|
5
|
+
* A generated standalone entry imports from here, so the code is unit-tested
|
|
6
|
+
* in TypeScript instead of being a string the adapter emits and nobody runs.
|
|
7
|
+
*/
|
|
8
|
+
export { DATA_DIR_ENV, PARENT_PID_ENV, watchParentProcess, } from './parent-watch.js';
|
|
9
|
+
export type { ParentWatch, ParentWatchOptions } from './parent-watch.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@pikku/deploy-standalone/runtime` — the sliver of this package that runs
|
|
3
|
+
* inside the shipped artifact rather than on the build machine.
|
|
4
|
+
*
|
|
5
|
+
* A generated standalone entry imports from here, so the code is unit-tested
|
|
6
|
+
* in TypeScript instead of being a string the adapter emits and nobody runs.
|
|
7
|
+
*/
|
|
8
|
+
export { DATA_DIR_ENV, PARENT_PID_ENV, watchParentProcess, } from './parent-watch.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable a desktop shell uses to tell its sidecar which process
|
|
3
|
+
* it must not outlive.
|
|
4
|
+
*/
|
|
5
|
+
export declare const PARENT_PID_ENV = "PIKKU_PARENT_PID";
|
|
6
|
+
/**
|
|
7
|
+
* Environment variable a desktop shell uses to tell its sidecar where the
|
|
8
|
+
* SQLite file, uploaded content and runtime state belong. The shell resolves
|
|
9
|
+
* it from the platform's own app-data location, because a binary launched by
|
|
10
|
+
* double-click has no meaningful working directory.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DATA_DIR_ENV = "PIKKU_DATA_DIR";
|
|
13
|
+
export type ParentWatchOptions = {
|
|
14
|
+
/** Where the parent pid is read from. Defaults to `process.env`. */
|
|
15
|
+
env?: Record<string, string | undefined>;
|
|
16
|
+
/** Probe for whether a pid is still running. Defaults to signal 0. */
|
|
17
|
+
isAlive?: (pid: number) => boolean;
|
|
18
|
+
/** Run when the parent is found to be gone. Defaults to exiting cleanly. */
|
|
19
|
+
onOrphaned?: () => void;
|
|
20
|
+
intervalMs?: number;
|
|
21
|
+
};
|
|
22
|
+
export type ParentWatch = {
|
|
23
|
+
/** False when no usable parent pid was supplied — the watch is inert. */
|
|
24
|
+
readonly watching: boolean;
|
|
25
|
+
readonly parentPid: number | undefined;
|
|
26
|
+
/** True only if the poll timer would hold the event loop open. */
|
|
27
|
+
readonly holdsProcessOpen: boolean;
|
|
28
|
+
/** Probe immediately rather than waiting for the next interval. */
|
|
29
|
+
checkNow(): void;
|
|
30
|
+
stop(): void;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Exit when the process that spawned us does.
|
|
34
|
+
*
|
|
35
|
+
* Tauri kills its sidecar on a clean exit, but a hard crash of the shell never
|
|
36
|
+
* runs that path. An orphaned pikku server keeps the SQLite file open, and the
|
|
37
|
+
* next launch — which single-instance only guards against a second *shell* —
|
|
38
|
+
* would be a second writer against the same database. Polling the parent is the only portable answer: neither
|
|
39
|
+
* `process.on('disconnect')` (no IPC channel here) nor a closed stdin is
|
|
40
|
+
* reliable across the platforms a desktop build targets.
|
|
41
|
+
*
|
|
42
|
+
* With no parent pid in the environment the watch is inert, so a server run
|
|
43
|
+
* from a terminal or a container behaves exactly as it did before.
|
|
44
|
+
*/
|
|
45
|
+
export declare const watchParentProcess: (options?: ParentWatchOptions) => ParentWatch;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable a desktop shell uses to tell its sidecar which process
|
|
3
|
+
* it must not outlive.
|
|
4
|
+
*/
|
|
5
|
+
export const PARENT_PID_ENV = 'PIKKU_PARENT_PID';
|
|
6
|
+
/**
|
|
7
|
+
* Environment variable a desktop shell uses to tell its sidecar where the
|
|
8
|
+
* SQLite file, uploaded content and runtime state belong. The shell resolves
|
|
9
|
+
* it from the platform's own app-data location, because a binary launched by
|
|
10
|
+
* double-click has no meaningful working directory.
|
|
11
|
+
*/
|
|
12
|
+
export const DATA_DIR_ENV = 'PIKKU_DATA_DIR';
|
|
13
|
+
/**
|
|
14
|
+
* A pid is alive if signalling it succeeds. `EPERM` also means alive — the
|
|
15
|
+
* process exists but belongs to another user — and only `ESRCH` means gone.
|
|
16
|
+
*/
|
|
17
|
+
const defaultIsAlive = (pid) => {
|
|
18
|
+
try {
|
|
19
|
+
process.kill(pid, 0);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return err.code === 'EPERM';
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const parsePid = (raw) => {
|
|
27
|
+
if (!raw)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (!/^\d+$/.test(raw))
|
|
30
|
+
return undefined;
|
|
31
|
+
const pid = Number(raw);
|
|
32
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Exit when the process that spawned us does.
|
|
36
|
+
*
|
|
37
|
+
* Tauri kills its sidecar on a clean exit, but a hard crash of the shell never
|
|
38
|
+
* runs that path. An orphaned pikku server keeps the SQLite file open, and the
|
|
39
|
+
* next launch — which single-instance only guards against a second *shell* —
|
|
40
|
+
* would be a second writer against the same database. Polling the parent is the only portable answer: neither
|
|
41
|
+
* `process.on('disconnect')` (no IPC channel here) nor a closed stdin is
|
|
42
|
+
* reliable across the platforms a desktop build targets.
|
|
43
|
+
*
|
|
44
|
+
* With no parent pid in the environment the watch is inert, so a server run
|
|
45
|
+
* from a terminal or a container behaves exactly as it did before.
|
|
46
|
+
*/
|
|
47
|
+
export const watchParentProcess = (options = {}) => {
|
|
48
|
+
const env = options.env ?? process.env;
|
|
49
|
+
const isAlive = options.isAlive ?? defaultIsAlive;
|
|
50
|
+
const onOrphaned = options.onOrphaned ?? (() => process.exit(0));
|
|
51
|
+
const intervalMs = options.intervalMs ?? 1_000;
|
|
52
|
+
const parentPid = parsePid(env[PARENT_PID_ENV]);
|
|
53
|
+
let timer;
|
|
54
|
+
let fired = false;
|
|
55
|
+
const stop = () => {
|
|
56
|
+
if (timer) {
|
|
57
|
+
clearInterval(timer);
|
|
58
|
+
timer = undefined;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const checkNow = () => {
|
|
62
|
+
if (parentPid === undefined || fired)
|
|
63
|
+
return;
|
|
64
|
+
if (isAlive(parentPid))
|
|
65
|
+
return;
|
|
66
|
+
fired = true;
|
|
67
|
+
stop();
|
|
68
|
+
onOrphaned();
|
|
69
|
+
};
|
|
70
|
+
if (parentPid !== undefined) {
|
|
71
|
+
timer = setInterval(checkNow, intervalMs);
|
|
72
|
+
// The watch is a guard, not a reason to stay running: a server that has
|
|
73
|
+
// finished its work must still be allowed to exit.
|
|
74
|
+
timer.unref?.();
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
get watching() {
|
|
78
|
+
return timer !== undefined;
|
|
79
|
+
},
|
|
80
|
+
parentPid,
|
|
81
|
+
get holdsProcessOpen() {
|
|
82
|
+
return timer?.hasRef?.() ?? false;
|
|
83
|
+
},
|
|
84
|
+
checkNow,
|
|
85
|
+
stop,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Directory the shell crate is generated into, relative to the project root. */
|
|
2
|
+
export declare const TAURI_SHELL_DIR = "src-tauri";
|
|
3
|
+
/**
|
|
4
|
+
* A reverse-DNS identifier for the bundle.
|
|
5
|
+
*
|
|
6
|
+
* A scoped package already names its org, so `@acme/shop` becomes
|
|
7
|
+
* `com.acme.shop`. An unscoped name has no org to borrow, and `com.shop.app`
|
|
8
|
+
* is not an option — macOS rejects an identifier ending in `.app`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const tauriBundleIdentifier: (packageName: string) => string;
|
|
11
|
+
export type GenerateTauriShellOptions = {
|
|
12
|
+
/** Project root. The crate is written to `<projectDir>/src-tauri`. */
|
|
13
|
+
projectDir: string;
|
|
14
|
+
/** Product name, and the `externalBin` base name of the sidecar. */
|
|
15
|
+
appName: string;
|
|
16
|
+
/** Reverse-DNS bundle identifier. See {@link tauriBundleIdentifier}. */
|
|
17
|
+
identifier: string;
|
|
18
|
+
version?: string;
|
|
19
|
+
windowTitle?: string;
|
|
20
|
+
width?: number;
|
|
21
|
+
height?: number;
|
|
22
|
+
/** The compiled pikku binary to install as the sidecar. */
|
|
23
|
+
binaryPath?: string;
|
|
24
|
+
/** Defaults to the host triple, via `rustc -vV` when available. */
|
|
25
|
+
targetTriple?: string;
|
|
26
|
+
/**
|
|
27
|
+
* An already-running server to open the window against, instead of shipping
|
|
28
|
+
* one. The shell then bundles nothing: no sidecar, no binary, no supervision.
|
|
29
|
+
*/
|
|
30
|
+
remoteUrl?: string;
|
|
31
|
+
};
|
|
32
|
+
export type GenerateTauriShellResult = {
|
|
33
|
+
/** Absolute path of the generated crate. */
|
|
34
|
+
dir: string;
|
|
35
|
+
/** Files written this run, relative to `dir`. */
|
|
36
|
+
written: string[];
|
|
37
|
+
/** Files left alone because the user has edited them, relative to `dir`. */
|
|
38
|
+
preserved: string[];
|
|
39
|
+
targetTriple: string;
|
|
40
|
+
sidecar?: {
|
|
41
|
+
fileName: string;
|
|
42
|
+
path: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export declare const generateTauriShell: (options: GenerateTauriShellOptions) => Promise<GenerateTauriShellResult>;
|