@pikku/deploy-standalone 0.12.11 → 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 +70 -0
- package/dist/adapter.d.ts +37 -45
- package/dist/adapter.js +187 -46
- package/dist/index.d.ts +1 -1
- 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 +10 -5
- package/src/adapter.test.ts +186 -0
- package/src/adapter.ts +216 -62
- package/src/desktop-deploy.test.ts +167 -0
- package/src/index.ts +1 -3
- 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,75 @@
|
|
|
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
|
+
|
|
44
|
+
## 0.12.12
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- 32616af: Give the deploy pipeline one shared contract instead of a copy per adapter
|
|
49
|
+
|
|
50
|
+
`DeploymentManifest`, `DeploymentUnit`, `EntryGenerationContext` and
|
|
51
|
+
`ProviderAdapter` were hand-copied into eleven source files across the four
|
|
52
|
+
provider adapters and the CLI — three copies inside `@pikku/deploy-cloudflare`
|
|
53
|
+
alone. Nothing compared the copies, so they had already drifted: several typed
|
|
54
|
+
`role` as a bare `string`, and none carried the manifest's addon-scoping fields.
|
|
55
|
+
|
|
56
|
+
They now live in a new zero-dependency `@pikku/deploy` package that every
|
|
57
|
+
adapter and the CLI import, and each adapter declares `implements
|
|
58
|
+
ProviderAdapter` so the compiler checks it against the contract it claims to
|
|
59
|
+
satisfy. That check immediately caught a real disagreement: the deploy result's
|
|
60
|
+
`workersDeployed` and `resourcesCreated` were `string[]` from Cloudflare — the
|
|
61
|
+
shape the result file and the generated SDK types already record — but
|
|
62
|
+
`Array<{ name: string }>` from the standalone adapter. Both are now `string[]`.
|
|
63
|
+
|
|
64
|
+
The Lambda and Azure adapters also derived their esbuild externals from a
|
|
65
|
+
hand-written list of 25 node builtins, so anything outside it (`async_hooks`,
|
|
66
|
+
`perf_hooks`, `timers`, `http2`, …) was bundled instead of resolved from the
|
|
67
|
+
runtime. They now use `nodeBuiltinExternals()`, which reads `builtinModules`
|
|
68
|
+
from the running Node and cannot fall behind it.
|
|
69
|
+
|
|
70
|
+
- Updated dependencies [32616af]
|
|
71
|
+
- @pikku/deploy@0.12.1
|
|
72
|
+
|
|
3
73
|
## 0.12.11
|
|
4
74
|
|
|
5
75
|
### Patch Changes
|
package/dist/adapter.d.ts
CHANGED
|
@@ -16,55 +16,48 @@
|
|
|
16
16
|
* compiles the bundle into a single self-contained executable via
|
|
17
17
|
* `bun build --compile`. No runtime needed on the target host.
|
|
18
18
|
*/
|
|
19
|
+
import type { EntryGenerationContext, ProviderAdapter } from '@pikku/deploy';
|
|
19
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";
|
|
20
33
|
export interface StandaloneProviderAdapterOptions {
|
|
21
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;
|
|
22
51
|
}
|
|
23
|
-
|
|
24
|
-
type: 'fetch';
|
|
25
|
-
routes: Array<{
|
|
26
|
-
method: string;
|
|
27
|
-
route: string;
|
|
28
|
-
pikkuFuncId: string;
|
|
29
|
-
}>;
|
|
30
|
-
} | {
|
|
31
|
-
type: 'queue';
|
|
32
|
-
queueName: string;
|
|
33
|
-
} | {
|
|
34
|
-
type: 'scheduled';
|
|
35
|
-
schedule: string;
|
|
36
|
-
taskName: string;
|
|
37
|
-
};
|
|
38
|
-
interface DeploymentUnit {
|
|
39
|
-
name: string;
|
|
40
|
-
role: string;
|
|
41
|
-
functionIds: string[];
|
|
42
|
-
services: Array<{
|
|
43
|
-
capability: string;
|
|
44
|
-
sourceServiceName: string;
|
|
45
|
-
}>;
|
|
46
|
-
dependsOn: string[];
|
|
47
|
-
handlers: DeploymentHandler[];
|
|
48
|
-
tags: string[];
|
|
49
|
-
}
|
|
50
|
-
interface EntryGenerationContext {
|
|
51
|
-
unit: DeploymentUnit;
|
|
52
|
-
unitDir: string;
|
|
53
|
-
bootstrapPath: string;
|
|
54
|
-
configImport: string;
|
|
55
|
-
configVar: string;
|
|
56
|
-
servicesImport: string;
|
|
57
|
-
servicesVar: string;
|
|
58
|
-
singletonServicesImport: string;
|
|
59
|
-
servicesType: string;
|
|
60
|
-
mcpImport: string;
|
|
61
|
-
mcpServerOption: string;
|
|
62
|
-
}
|
|
63
|
-
export declare class StandaloneProviderAdapter {
|
|
52
|
+
export declare class StandaloneProviderAdapter implements ProviderAdapter {
|
|
64
53
|
readonly name = "standalone";
|
|
65
54
|
readonly deployDirName = "standalone";
|
|
66
55
|
readonly singleUnit = true;
|
|
67
56
|
readonly runtime: StandaloneRuntime;
|
|
57
|
+
readonly desktop: boolean;
|
|
58
|
+
readonly projectDir?: string;
|
|
59
|
+
readonly desktopIdentifier?: string;
|
|
60
|
+
readonly desktopUrl?: string;
|
|
68
61
|
constructor(options?: StandaloneProviderAdapterOptions);
|
|
69
62
|
generateEntrySource(ctx: EntryGenerationContext): string;
|
|
70
63
|
private generateNodeEntrySource;
|
|
@@ -89,13 +82,12 @@ export declare class StandaloneProviderAdapter {
|
|
|
89
82
|
}[];
|
|
90
83
|
workersDeployed?: undefined;
|
|
91
84
|
resourcesCreated?: undefined;
|
|
85
|
+
targetTriple?: undefined;
|
|
92
86
|
} | {
|
|
93
87
|
success: boolean;
|
|
94
|
-
workersDeployed:
|
|
95
|
-
name: string;
|
|
96
|
-
}[];
|
|
88
|
+
workersDeployed: string[];
|
|
97
89
|
resourcesCreated: never[];
|
|
98
90
|
errors: never[];
|
|
91
|
+
targetTriple: string | undefined;
|
|
99
92
|
}>;
|
|
100
93
|
}
|
|
101
|
-
export {};
|
package/dist/adapter.js
CHANGED
|
@@ -1,28 +1,60 @@
|
|
|
1
|
+
import { nodeBuiltinExternals, SERVER_READY_MARKER } from '@pikku/deploy';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
16
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
|
|
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.
|
|
18
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
|
+
};
|
|
19
43
|
export class StandaloneProviderAdapter {
|
|
20
44
|
name = 'standalone';
|
|
21
45
|
deployDirName = 'standalone';
|
|
22
46
|
singleUnit = true;
|
|
23
47
|
runtime;
|
|
48
|
+
desktop;
|
|
49
|
+
projectDir;
|
|
50
|
+
desktopIdentifier;
|
|
51
|
+
desktopUrl;
|
|
24
52
|
constructor(options = {}) {
|
|
25
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;
|
|
26
58
|
}
|
|
27
59
|
generateEntrySource(ctx) {
|
|
28
60
|
if (this.runtime === 'bun') {
|
|
@@ -35,12 +67,19 @@ export class StandaloneProviderAdapter {
|
|
|
35
67
|
`// Generated standalone entry — all functions in one process`,
|
|
36
68
|
`import { LocalEventHubService } from '@pikku/core/channel/local'`,
|
|
37
69
|
`import { ConsoleLogger, InMemoryQueueService, InMemoryTriggerService, InMemoryWorkflowService } from '@pikku/core/services'`,
|
|
38
|
-
`import { pikkuState } from '@pikku/core/
|
|
39
|
-
`import { wireAgentScorerQueueWorkers } from '@pikku/core/
|
|
70
|
+
`import { pikkuState } from '@pikku/core/state'`,
|
|
71
|
+
`import { wireAgentScorerQueueWorkers } from '@pikku/core/agent-scorer'`,
|
|
40
72
|
`import { InMemorySchedulerService } from '@pikku/schedule'`,
|
|
41
73
|
`import { PikkuNodeHTTPServer } from '@pikku/node-http-server'`,
|
|
42
74
|
`import { DEFAULT_WS_MAX_PAYLOAD, pikkuWebsocketHandler } from '@pikku/ws'`,
|
|
43
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
|
+
: []),
|
|
44
83
|
``,
|
|
45
84
|
ctx.configImport,
|
|
46
85
|
ctx.servicesImport,
|
|
@@ -72,9 +111,21 @@ export class StandaloneProviderAdapter {
|
|
|
72
111
|
` })`,
|
|
73
112
|
` pikkuState(null, 'package', 'singletonServices', singletonServices)`,
|
|
74
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
|
+
: []),
|
|
75
126
|
` const wss = new WebSocketServer({ noServer: true, maxPayload: DEFAULT_WS_MAX_PAYLOAD })`,
|
|
76
127
|
` const server = new PikkuNodeHTTPServer(`,
|
|
77
|
-
` { ...config, port, hostname },`,
|
|
128
|
+
` { ...config, port, hostname${ctx.frontend ? ', staticMounts' : ''} },`,
|
|
78
129
|
` logger,`,
|
|
79
130
|
` {`,
|
|
80
131
|
` ${ctx.mcpServerOption}configureServer: (httpServer) => {`,
|
|
@@ -87,6 +138,7 @@ export class StandaloneProviderAdapter {
|
|
|
87
138
|
` await triggerService.start()`,
|
|
88
139
|
` server.enableExitOnSignals()`,
|
|
89
140
|
` await server.start()`,
|
|
141
|
+
...sidecarHandshakeLines(),
|
|
90
142
|
`}`,
|
|
91
143
|
``,
|
|
92
144
|
`main().catch((err) => {`,
|
|
@@ -100,10 +152,14 @@ export class StandaloneProviderAdapter {
|
|
|
100
152
|
return [
|
|
101
153
|
`// Generated standalone entry (bun runtime) — all functions in one process`,
|
|
102
154
|
`import { ConsoleLogger, InMemoryQueueService, InMemoryTriggerService, InMemoryWorkflowService } from '@pikku/core/services'`,
|
|
103
|
-
|
|
104
|
-
`import {
|
|
155
|
+
SIDECAR_RUNTIME_IMPORT,
|
|
156
|
+
`import { pikkuState } from '@pikku/core/state'`,
|
|
157
|
+
`import { wireAgentScorerQueueWorkers } from '@pikku/core/agent-scorer'`,
|
|
105
158
|
`import { InMemorySchedulerService } from '@pikku/schedule'`,
|
|
106
159
|
`import { PikkuBunServer, BunEventHubService } from '@pikku/bun-server'`,
|
|
160
|
+
...(ctx.frontend
|
|
161
|
+
? [`import { frontendAssets } from '${STANDALONE_FRONTEND_MANIFEST}'`]
|
|
162
|
+
: []),
|
|
107
163
|
``,
|
|
108
164
|
ctx.configImport,
|
|
109
165
|
ctx.servicesImport,
|
|
@@ -135,12 +191,26 @@ export class StandaloneProviderAdapter {
|
|
|
135
191
|
` })`,
|
|
136
192
|
` pikkuState(null, 'package', 'singletonServices', singletonServices)`,
|
|
137
193
|
``,
|
|
138
|
-
|
|
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 })`,
|
|
139
208
|
` await server.init()`,
|
|
140
209
|
` await schedulerService.start()`,
|
|
141
210
|
` await triggerService.start()`,
|
|
142
211
|
` server.enableExitOnSignals()`,
|
|
143
212
|
` await server.start()`,
|
|
213
|
+
...sidecarHandshakeLines(),
|
|
144
214
|
`}`,
|
|
145
215
|
``,
|
|
146
216
|
`main().catch((err) => {`,
|
|
@@ -160,33 +230,12 @@ export class StandaloneProviderAdapter {
|
|
|
160
230
|
return new Map();
|
|
161
231
|
}
|
|
162
232
|
getExternals() {
|
|
163
|
-
const externals =
|
|
164
|
-
'node:*',
|
|
165
|
-
'child_process',
|
|
166
|
-
'crypto',
|
|
167
|
-
'fs',
|
|
168
|
-
'http',
|
|
169
|
-
'https',
|
|
170
|
-
'net',
|
|
171
|
-
'os',
|
|
172
|
-
'path',
|
|
173
|
-
'stream',
|
|
174
|
-
'url',
|
|
175
|
-
'util',
|
|
176
|
-
'zlib',
|
|
177
|
-
'events',
|
|
178
|
-
'buffer',
|
|
179
|
-
'querystring',
|
|
180
|
-
'tls',
|
|
181
|
-
'dns',
|
|
182
|
-
'dgram',
|
|
183
|
-
'cluster',
|
|
184
|
-
'worker_threads',
|
|
185
|
-
];
|
|
233
|
+
const externals = nodeBuiltinExternals();
|
|
186
234
|
if (this.runtime === 'bun') {
|
|
187
235
|
// Bun-native builtins are provided by the runtime and resolved by
|
|
188
236
|
// `bun build --compile` — leave them as imports rather than inlining.
|
|
189
237
|
externals.push('bun', 'bun:*', 'bun:sqlite', 'bun:ffi');
|
|
238
|
+
externals.push(STANDALONE_FRONTEND_MANIFEST);
|
|
190
239
|
}
|
|
191
240
|
return externals;
|
|
192
241
|
}
|
|
@@ -195,8 +244,34 @@ export class StandaloneProviderAdapter {
|
|
|
195
244
|
}
|
|
196
245
|
async deploy(options) {
|
|
197
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
|
+
}
|
|
198
273
|
const { join, dirname } = await import('node:path');
|
|
199
|
-
const { readdir, writeFile, copyFile, mkdir } = await import('node:fs/promises');
|
|
274
|
+
const { cp, readdir, writeFile, copyFile, mkdir } = await import('node:fs/promises');
|
|
200
275
|
const { existsSync } = await import('node:fs');
|
|
201
276
|
// Find the unit dir with the bundle
|
|
202
277
|
const entries = await readdir(buildDir);
|
|
@@ -218,6 +293,21 @@ export class StandaloneProviderAdapter {
|
|
|
218
293
|
await copyFile(join(unitDir, 'bundle.js.map'), join(outDir, 'bundle.js.map'));
|
|
219
294
|
}
|
|
220
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
|
+
}
|
|
221
311
|
// --- 2b. bun runtime: compile the bundle into a self-contained binary ---
|
|
222
312
|
if (this.runtime === 'bun') {
|
|
223
313
|
const { execFileSync } = await import('node:child_process');
|
|
@@ -245,6 +335,56 @@ export class StandaloneProviderAdapter {
|
|
|
245
335
|
};
|
|
246
336
|
}
|
|
247
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
|
+
}
|
|
248
388
|
// --- 3. config/ — empty template with .env example ---
|
|
249
389
|
const configDir = join(outDir, 'config');
|
|
250
390
|
await mkdir(configDir, { recursive: true });
|
|
@@ -264,9 +404,10 @@ export class StandaloneProviderAdapter {
|
|
|
264
404
|
logger.info(`Output: ${outDir}`);
|
|
265
405
|
return {
|
|
266
406
|
success: true,
|
|
267
|
-
workersDeployed: [
|
|
407
|
+
workersDeployed: [appName],
|
|
268
408
|
resourcesCreated: [],
|
|
269
409
|
errors: [],
|
|
410
|
+
targetTriple,
|
|
270
411
|
};
|
|
271
412
|
}
|
|
272
413
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,5 +11,5 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { StandaloneProviderAdapter, type StandaloneProviderAdapterOptions } from './adapter.js';
|
|
13
13
|
export { StandaloneProviderAdapter };
|
|
14
|
-
export type { StandaloneProviderAdapterOptions
|
|
14
|
+
export type { StandaloneProviderAdapterOptions } from './adapter.js';
|
|
15
15
|
export declare const createAdapter: (options?: StandaloneProviderAdapterOptions) => StandaloneProviderAdapter;
|
|
@@ -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
|
+
};
|