@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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: A remote desktop shell bundles nothing
|
|
4
|
+
description: --desktop-url produces a window onto an already-deployed server — no sidecar, no binary, no bun requirement — with the window declared in tauri.conf.json rather than opened from Rust
|
|
5
|
+
tags: [tauri, desktop, standalone, remote]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# A remote desktop shell bundles nothing
|
|
9
|
+
|
|
10
|
+
`pikku deploy apply --desktop-url https://app.example.com` generates the same
|
|
11
|
+
`src-tauri/` crate as the sidecar shell, minus everything that exists to run a
|
|
12
|
+
server: no `externalBin`, no `tauri-plugin-shell`, no binary copied into
|
|
13
|
+
`binaries/`, and no `--runtime bun` requirement, because there is nothing to
|
|
14
|
+
compile. The app is a window onto a server someone else's deploy already put on
|
|
15
|
+
the internet.
|
|
16
|
+
|
|
17
|
+
The window is **declared in `tauri.conf.json`**, not built from Rust. That is
|
|
18
|
+
the whole difference in the generated program. A sidecar binds its port at
|
|
19
|
+
launch, so its origin is unknown until it says so and only Rust can open the
|
|
20
|
+
window; a remote url is known before a line of Rust is written, so `main.rs`
|
|
21
|
+
keeps nothing but the single-instance plugin.
|
|
22
|
+
|
|
23
|
+
The origin rule from
|
|
24
|
+
[the sidecar shell](the-desktop-shell-runs-the-server-as-a-sidecar.md) is
|
|
25
|
+
unchanged and is why this mode is worth having at all: the webview loads the
|
|
26
|
+
real `https://` origin, so cookies are first-party, there is no CORS, and OAuth
|
|
27
|
+
redirects land where the server expects. A shell that instead bundled the
|
|
28
|
+
frontend and served it from `tauri://localhost` would break every one of those
|
|
29
|
+
— which is exactly what "remote origin only" rules out.
|
|
30
|
+
|
|
31
|
+
The url is validated as `http:` or `https:` at generate time. Anything else
|
|
32
|
+
builds a crate that fails at runtime, in a window with no address bar to
|
|
33
|
+
diagnose it from.
|
|
34
|
+
|
|
35
|
+
**What this rules out:** bundling the frontend into the shell, a shell that
|
|
36
|
+
falls back to a local server when the remote is unreachable, and mixing the two
|
|
37
|
+
modes — a binary and a url together is refused rather than silently shipping a
|
|
38
|
+
sidecar nothing starts.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Deploy consumes a built frontend, it does not build one
|
|
4
|
+
description: pikku reads an already-built client directory named by the frontend config key; running the frontend's build command is the project's job
|
|
5
|
+
tags: [frontend, deploy, standalone, cli]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Deploy consumes a built frontend, it does not build one
|
|
9
|
+
|
|
10
|
+
`frontend` in `pikku.config.json` names a **directory of built output** —
|
|
11
|
+
`{ dir: './web/dist', urlPrefix: '/', spaFallback: true }` — not a project to
|
|
12
|
+
build. `pikku deploy` reads that directory. It does not run `vite build`, does
|
|
13
|
+
not shell out to a package manager, and fails with a plain error if the
|
|
14
|
+
directory is absent rather than trying to produce it.
|
|
15
|
+
|
|
16
|
+
Building the frontend would mean pikku deciding which package manager runs,
|
|
17
|
+
which script name means "build", which workspace the frontend lives in, what
|
|
18
|
+
environment variables it needs, and what to do when that build fails inside a
|
|
19
|
+
deploy. Every one of those is a project-level answer that pikku would be
|
|
20
|
+
guessing at, and guessing wrong is worse than not trying: a deploy that
|
|
21
|
+
silently rebuilds can ship output that differs from what the project's own CI
|
|
22
|
+
verified. `yarn build && pikku deploy` states the order explicitly and keeps the
|
|
23
|
+
two steps independently debuggable.
|
|
24
|
+
|
|
25
|
+
The ordering constraint that this creates is real and worth naming. The bun path
|
|
26
|
+
embeds assets by generating a manifest of static imports, which means the build
|
|
27
|
+
sequence is fixed: **frontend build → manifest generation → server bundle →
|
|
28
|
+
`bun build --compile`**. A frontend that has not been built yet cannot be
|
|
29
|
+
enumerated, so there is no arrangement in which pikku could usefully build it
|
|
30
|
+
later.
|
|
31
|
+
|
|
32
|
+
**What this rules out:** a `frontend.build` command in the config, and any
|
|
33
|
+
deploy step that invokes a package manager on the user's behalf.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Desktop builds are unsigned and never update themselves
|
|
4
|
+
description: Code signing, notarization and auto-update are deliberately absent from the first version of the Tauri shell — a known limitation, not an oversight
|
|
5
|
+
tags: [tauri, desktop, limitations, distribution]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Desktop builds are unsigned and never update themselves
|
|
9
|
+
|
|
10
|
+
The generated shell has no signing pipeline and no updater. Both were left out
|
|
11
|
+
on purpose, and both need building before anything is distributed to people who
|
|
12
|
+
did not compile it themselves.
|
|
13
|
+
|
|
14
|
+
What this means in practice today:
|
|
15
|
+
|
|
16
|
+
- **macOS Gatekeeper will refuse the app on first launch.** An unsigned,
|
|
17
|
+
un-notarized `.app` downloaded from anywhere gets quarantined; the user has to
|
|
18
|
+
right-click → Open, or clear the quarantine attribute by hand. This is
|
|
19
|
+
accepted for now.
|
|
20
|
+
- **Windows SmartScreen shows an unknown-publisher warning** for the same reason.
|
|
21
|
+
- **There is no update path.** A shipped build stays the version it was. Tauri's
|
|
22
|
+
updater plugin is not wired in, no update endpoint exists, and — relevant
|
|
23
|
+
later — the updater requires signing keys, so the two gaps have to be closed
|
|
24
|
+
together rather than in either order.
|
|
25
|
+
|
|
26
|
+
Signing is not something the generator can quietly grow, because it needs
|
|
27
|
+
secrets the build machine has to hold: an Apple Developer ID certificate plus an
|
|
28
|
+
app-specific password or API key for notarization, and an Authenticode
|
|
29
|
+
certificate on Windows. That is CI configuration and key custody, not code
|
|
30
|
+
generation, which is why it is a separate piece of work rather than a flag.
|
|
31
|
+
|
|
32
|
+
**What this rules out for now:** distributing a build to end users without
|
|
33
|
+
telling them how to get past Gatekeeper, and any claim that a shipped desktop
|
|
34
|
+
app can be patched after release.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: overview
|
|
3
|
+
title: Decisions
|
|
4
|
+
description: What "a standalone unit with a UI" does and does not mean, and what a double-clickable build of it is
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Decisions
|
|
8
|
+
|
|
9
|
+
<!-- pikku:knowledge-index -->
|
|
10
|
+
|
|
11
|
+
- [A remote desktop shell bundles nothing](a-remote-desktop-shell-bundles-nothing.md) — --desktop-url produces a window onto an already-deployed server — no sidecar, no binary, no bun requirement — with the window declared in tauri.conf.json rather than opened from Rust
|
|
12
|
+
- [A pikku server serves a static frontend, not a rendered one](a-pikku-server-serves-a-static-frontend.md) — The frontend is TanStack Start built to static output and served through a static mount; pikku never runs a framework renderer in-process
|
|
13
|
+
- [Deploy consumes a built frontend, it does not build one](deploy-consumes-a-built-frontend.md) — pikku reads an already-built client directory named by the frontend config key; running the frontend's build command is the project's job
|
|
14
|
+
- [Desktop builds are unsigned and never update themselves](desktop-builds-are-unsigned-and-never-update-themselves.md) — Code signing, notarization and auto-update are deliberately absent from the first version of the Tauri shell — a known limitation, not an oversight
|
|
15
|
+
- [Standalone assets are embedded in the bun binary](standalone-assets-are-embedded-in-the-bun-binary.md) — A generated manifest of `with { type: 'file' }` imports puts the frontend inside the compiled binary; the imports must be static literals, which fixes the build order
|
|
16
|
+
- [The desktop shell runs the server as a sidecar, not embedded](the-desktop-shell-runs-the-server-as-a-sidecar.md) — Tauri spawns the compiled pikku binary and points the webview at its HTTP origin, so cookies, CORS and OAuth behave exactly as they do in a browser
|
|
17
|
+
- [The sidecar reports its port; the shell never picks one](the-sidecar-reports-its-port-the-shell-never-picks-one.md) — The server binds :0 and prints the port it got on the ready line; Rust blocks on that line rather than choosing a free port and passing it down
|
|
18
|
+
|
|
19
|
+
<!-- /pikku:knowledge-index -->
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: Standalone assets are embedded in the bun binary
|
|
4
|
+
description: A generated manifest of `with { type: 'file' }` imports puts the frontend inside the compiled binary; the imports must be static literals, which fixes the build order
|
|
5
|
+
tags: [frontend, standalone, bun, assets]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Standalone assets are embedded in the bun binary
|
|
9
|
+
|
|
10
|
+
On the bun runtime the frontend ships **inside** the compiled binary rather than
|
|
11
|
+
beside it. `bun build --compile` embeds any module imported with
|
|
12
|
+
`with { type: 'file' }`, and `Bun.file()` reads the embedded path back at
|
|
13
|
+
runtime. That is what makes the artifact a single file you can hand someone,
|
|
14
|
+
which was the whole premise of the feature.
|
|
15
|
+
|
|
16
|
+
This was verified rather than assumed: a test binary was compiled, its `assets/`
|
|
17
|
+
directory and entry source deleted from disk, and the binary then served both
|
|
18
|
+
`/` and a hashed asset correctly, with `Bun.embeddedFiles` reporting both files.
|
|
19
|
+
Two things fell out of that check. Content types are inferred by `Bun.file` for
|
|
20
|
+
free, so the bun path needs no MIME table of its own. And the binary is large —
|
|
21
|
+
around 64MB for a trivial app — because the bun runtime is in there; that is the
|
|
22
|
+
cost of the single-file property, not a bug to optimize away.
|
|
23
|
+
|
|
24
|
+
The constraint that shapes the code is that **`with { type: 'file' }` cannot be
|
|
25
|
+
dynamic**. There is no way to embed a directory, or to build the import list at
|
|
26
|
+
runtime; each file needs its own literal `import` statement. So a generated
|
|
27
|
+
manifest module enumerates them, and generating it requires the built frontend
|
|
28
|
+
to already exist — see
|
|
29
|
+
[deploy consumes a built frontend](deploy-consumes-a-built-frontend.md).
|
|
30
|
+
|
|
31
|
+
Because assets live in the binary on one runtime and on disk on another,
|
|
32
|
+
`StaticMount` carries an optional `assets: Record<string, string>` map. A mount
|
|
33
|
+
with `assets` resolves keys through it; a mount without one resolves them against
|
|
34
|
+
`directory`. One mount type, one pipeline, and the node path stays exactly as it
|
|
35
|
+
was.
|
|
36
|
+
|
|
37
|
+
**What this rules out:** shipping the frontend as a sibling directory next to the
|
|
38
|
+
binary, a runtime-assembled embed list, and a second static-serving code path for
|
|
39
|
+
embedded assets.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The desktop shell runs the server as a sidecar, not embedded
|
|
4
|
+
description: Tauri spawns the compiled pikku binary and points the webview at its HTTP origin, so cookies, CORS and OAuth behave exactly as they do in a browser
|
|
5
|
+
tags: [tauri, desktop, standalone, bun]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The desktop shell runs the server as a sidecar, not embedded
|
|
9
|
+
|
|
10
|
+
`pikku deploy apply --provider standalone --runtime bun --desktop` generates a
|
|
11
|
+
`src-tauri/` crate that ships the compiled binary as an `externalBin`, spawns it
|
|
12
|
+
at launch, and opens a window at `http://127.0.0.1:<port>`. The server serves
|
|
13
|
+
both the API and the built frontend, so **the UI and the API share one real HTTP
|
|
14
|
+
origin**.
|
|
15
|
+
|
|
16
|
+
That single property is the whole reason for the design. A webview loaded from
|
|
17
|
+
`tauri://localhost` is a different origin from the server it talks to, and
|
|
18
|
+
everything keyed on `window.location.origin` breaks: cookies stop being
|
|
19
|
+
first-party, every request needs CORS, better-auth needs special-casing, and
|
|
20
|
+
OAuth redirects have nowhere valid to land. Pointing the webview at the server's
|
|
21
|
+
own origin means none of that is true — the app is the same app it is on the
|
|
22
|
+
web, and no auth code knows it is running on a desktop.
|
|
23
|
+
|
|
24
|
+
The user never writes Rust. `main.rs`, `tauri.conf.json`, `Cargo.toml`,
|
|
25
|
+
`build.rs`, a placeholder icon and a placeholder frontend directory are all
|
|
26
|
+
generated, with the product name and bundle identifier taken from the project
|
|
27
|
+
rather than hardcoded. Regenerating is idempotent, and a file the user has since
|
|
28
|
+
edited is left alone and reported rather than overwritten — the generator
|
|
29
|
+
records a hash of what it wrote, which is the only way to tell "unchanged since
|
|
30
|
+
we wrote it" from "the user has taken this over".
|
|
31
|
+
|
|
32
|
+
Two supporting rules fall out of running a real server process:
|
|
33
|
+
|
|
34
|
+
- **Single instance is load-bearing.** `tauri-plugin-single-instance` focuses the
|
|
35
|
+
existing window instead of launching again. Two shells would mean two
|
|
36
|
+
sidecars: two SQLite writers on one file.
|
|
37
|
+
- **The sidecar must not outlive the shell.** Tauri stops it on a clean exit, but
|
|
38
|
+
a hard crash never runs that path, and an orphan holds the database open. The
|
|
39
|
+
shell passes its pid down as
|
|
40
|
+
`PIKKU_PARENT_PID` and the server polls it, exiting when the parent is gone.
|
|
41
|
+
With no such variable set — a terminal, a container — the watch is inert.
|
|
42
|
+
|
|
43
|
+
The shell also resolves the platform's app-data directory and passes it as
|
|
44
|
+
`PIKKU_DATA_DIR`. A double-clicked app has no meaningful working directory, so
|
|
45
|
+
that variable is where the SQLite file, uploaded content and runtime state live;
|
|
46
|
+
the server reads it in bootstrap, which is the one place `process.env` is
|
|
47
|
+
allowed.
|
|
48
|
+
|
|
49
|
+
**What this rules out:** linking the server into the Rust binary, serving the UI
|
|
50
|
+
from `tauri://localhost` or a custom protocol, a second auth path for desktop
|
|
51
|
+
builds, and any design where two windows can be open at once.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: decision
|
|
3
|
+
title: The sidecar reports its port; the shell never picks one
|
|
4
|
+
description: The server binds :0 and prints the port it got on the ready line; Rust blocks on that line rather than choosing a free port and passing it down
|
|
5
|
+
tags: [tauri, desktop, ports, server-ready]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# The sidecar reports its port; the shell never picks one
|
|
9
|
+
|
|
10
|
+
The shell starts the sidecar with `PORT=0`, reads its stdout until the ready
|
|
11
|
+
line appears, parses the port out of it, and only then creates the window.
|
|
12
|
+
|
|
13
|
+
The obvious alternative — have Rust find a free port and pass it down — has a
|
|
14
|
+
race with no fix: between the check that a port is free and the sidecar's bind,
|
|
15
|
+
anything on the machine can take it. Binding first and reporting back is the
|
|
16
|
+
only ordering with no window in it.
|
|
17
|
+
|
|
18
|
+
The line it waits for is the existing readiness marker:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
pikku: ready on http://127.0.0.1:53422
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`SERVER_READY_MARKER` now lives in `@pikku/deploy` rather than in the CLI,
|
|
25
|
+
because the two ends of the handshake are built by different packages: the CLI
|
|
26
|
+
waits on it for `pikku dev --spawn`, and the standalone provider's generated
|
|
27
|
+
entry prints it from inside the shipped binary. The CLI re-exports it from its
|
|
28
|
+
old path, so nothing that imported it had to change. A second copy of the string
|
|
29
|
+
would have drifted the first time either side touched it.
|
|
30
|
+
|
|
31
|
+
Making the line true required a real bound port to report. `--port 0` used to
|
|
32
|
+
print `:0`, because both `serve` and `dev` logged the *requested* port. Both
|
|
33
|
+
runtimes now expose the port they actually bound —
|
|
34
|
+
`PikkuNodeHTTPServer.port` reads `server.address()`, `PikkuBunServer.port`
|
|
35
|
+
already had it — `DevServerInstance` carries it, and every URL announced after
|
|
36
|
+
`start()` is built from it.
|
|
37
|
+
|
|
38
|
+
Note that `listening on …` is still **not** readiness. It is printed inside
|
|
39
|
+
`server.start()`, before the project's `afterStart` has run, so a parent that
|
|
40
|
+
treats it as ready races whatever the project seeds there.
|
|
41
|
+
|
|
42
|
+
**What this rules out:** a fixed default port for desktop builds, port
|
|
43
|
+
allocation in Rust, a handshake over a file or a socket instead of stdout, and
|
|
44
|
+
treating the runtime's own `listening on …` line as ready.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: overview
|
|
3
|
+
title: Knowledge
|
|
4
|
+
description: How a standalone unit comes to contain a frontend as well as a server, and how it becomes a double-clickable desktop app
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Knowledge
|
|
8
|
+
|
|
9
|
+
A standalone unit is meant to be one artifact you can hand someone: a binary
|
|
10
|
+
that, when run, is the whole application. Adding a UI to that picture forces
|
|
11
|
+
three questions — what kind of frontend, who builds it, and where the files
|
|
12
|
+
live at runtime — and the answers are less obvious than they look.
|
|
13
|
+
|
|
14
|
+
Wrapping that binary in a window forces a second set: who starts the server,
|
|
15
|
+
how the window learns the port it bound, who holds the passphrase, and what
|
|
16
|
+
happens to the server when the window goes away.
|
|
17
|
+
|
|
18
|
+
These notes record the answers and the constraints that produced them.
|
|
19
|
+
|
|
20
|
+
<!-- pikku:knowledge-index -->
|
|
21
|
+
- [decisions](decisions/index.md) — a rule that was chosen, and what it rules out
|
|
22
|
+
<!-- /pikku:knowledge-index -->
|
package/package.json
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/deploy-standalone",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.13",
|
|
4
4
|
"description": "Standalone deploy adapter for Pikku — bundles a project into a node bundle or a compiled bun executable",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc -b",
|
|
9
|
-
"tsc": "tsc --noEmit"
|
|
9
|
+
"tsc": "tsc --noEmit",
|
|
10
|
+
"test": "node --test --import tsx src/*.test.ts"
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
12
13
|
"typescript": "^6.0.3"
|
|
13
14
|
},
|
|
14
15
|
"exports": {
|
|
15
|
-
".": "./dist/index.js"
|
|
16
|
+
".": "./dist/index.js",
|
|
17
|
+
"./runtime": "./dist/runtime/index.js"
|
|
16
18
|
},
|
|
17
19
|
"license": "MIT",
|
|
18
20
|
"dependencies": {
|
|
19
|
-
"@pikku/deploy": "^0.12.
|
|
21
|
+
"@pikku/deploy": "^0.12.2"
|
|
20
22
|
}
|
|
21
23
|
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { after, describe, test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { existsSync, rmSync } from 'node:fs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { StandaloneProviderAdapter } from './adapter.js'
|
|
9
|
+
|
|
10
|
+
const baseContext = {
|
|
11
|
+
unit: { name: 'app', role: 'function' },
|
|
12
|
+
unitDir: '/build/app',
|
|
13
|
+
bootstrapPath: './.pikku/pikku-bootstrap.gen.js',
|
|
14
|
+
configImport: `import { createConfig } from './config.js'`,
|
|
15
|
+
configVar: 'createConfig',
|
|
16
|
+
servicesImport: `import { createSingletonServices } from './services.js'`,
|
|
17
|
+
servicesVar: 'createSingletonServices',
|
|
18
|
+
singletonServicesImport: '',
|
|
19
|
+
servicesType: 'Record<string, unknown>',
|
|
20
|
+
mcpImport: '',
|
|
21
|
+
mcpServerOption: '',
|
|
22
|
+
} as never
|
|
23
|
+
|
|
24
|
+
const withFrontend = {
|
|
25
|
+
...(baseContext as object),
|
|
26
|
+
frontend: { urlPrefix: '/', spaFallback: true },
|
|
27
|
+
} as never
|
|
28
|
+
|
|
29
|
+
describe('StandaloneProviderAdapter frontend serving', () => {
|
|
30
|
+
test('a node entry without a frontend mounts nothing', () => {
|
|
31
|
+
const source = new StandaloneProviderAdapter({
|
|
32
|
+
runtime: 'node',
|
|
33
|
+
}).generateEntrySource(baseContext)
|
|
34
|
+
|
|
35
|
+
assert.doesNotMatch(source, /staticMounts/)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('a bun entry without a frontend imports no asset manifest', () => {
|
|
39
|
+
const source = new StandaloneProviderAdapter({
|
|
40
|
+
runtime: 'bun',
|
|
41
|
+
}).generateEntrySource(baseContext)
|
|
42
|
+
|
|
43
|
+
assert.doesNotMatch(source, /frontend-assets/)
|
|
44
|
+
assert.doesNotMatch(source, /staticMounts/)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('a node entry serves the frontend from a directory beside the bundle', () => {
|
|
48
|
+
const source = new StandaloneProviderAdapter({
|
|
49
|
+
runtime: 'node',
|
|
50
|
+
}).generateEntrySource(withFrontend)
|
|
51
|
+
|
|
52
|
+
assert.match(source, /staticMounts/)
|
|
53
|
+
assert.match(
|
|
54
|
+
source,
|
|
55
|
+
/import\.meta\.url/,
|
|
56
|
+
'the directory must be resolved from the running bundle, not the build machine'
|
|
57
|
+
)
|
|
58
|
+
assert.doesNotMatch(
|
|
59
|
+
source,
|
|
60
|
+
/assets:/,
|
|
61
|
+
'node reads the copied directory rather than an embed map'
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('a bun entry serves the frontend from the embedded asset map', () => {
|
|
66
|
+
const source = new StandaloneProviderAdapter({
|
|
67
|
+
runtime: 'bun',
|
|
68
|
+
}).generateEntrySource(withFrontend)
|
|
69
|
+
|
|
70
|
+
assert.match(source, /from '\.\/frontend-assets\.gen\.js'/)
|
|
71
|
+
assert.match(source, /assets:/)
|
|
72
|
+
assert.doesNotMatch(
|
|
73
|
+
source,
|
|
74
|
+
/import\.meta\.url/,
|
|
75
|
+
'an embedded asset has no directory to resolve'
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('the mount carries the configured prefix and fallback', () => {
|
|
80
|
+
const source = new StandaloneProviderAdapter({
|
|
81
|
+
runtime: 'bun',
|
|
82
|
+
}).generateEntrySource({
|
|
83
|
+
...(baseContext as object),
|
|
84
|
+
frontend: { urlPrefix: '/app', spaFallback: false },
|
|
85
|
+
} as never)
|
|
86
|
+
|
|
87
|
+
assert.match(source, /urlPrefix: '\/app'/)
|
|
88
|
+
assert.match(source, /spaFallback: false/)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('bun externalises the asset manifest so esbuild never parses it', () => {
|
|
92
|
+
// esbuild rejects `with { type: 'file' }` outright; the manifest has to
|
|
93
|
+
// survive to the `bun build --compile` step untouched.
|
|
94
|
+
const externals = new StandaloneProviderAdapter({
|
|
95
|
+
runtime: 'bun',
|
|
96
|
+
}).getExternals()
|
|
97
|
+
|
|
98
|
+
assert.ok(externals.includes('./frontend-assets.gen.js'))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('the node runtime has no manifest to externalise', () => {
|
|
102
|
+
const externals = new StandaloneProviderAdapter({
|
|
103
|
+
runtime: 'node',
|
|
104
|
+
}).getExternals()
|
|
105
|
+
|
|
106
|
+
assert.ok(!externals.includes('./frontend-assets.gen.js'))
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('StandaloneProviderAdapter deploy output', () => {
|
|
111
|
+
const tempDirs: string[] = []
|
|
112
|
+
|
|
113
|
+
after(() => {
|
|
114
|
+
for (const dir of tempDirs) {
|
|
115
|
+
rmSync(dir, { recursive: true, force: true })
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const builtUnit = async (options: { withFrontend: boolean }) => {
|
|
120
|
+
const buildDir = await mkdtemp(join(tmpdir(), 'pikku-standalone-'))
|
|
121
|
+
tempDirs.push(buildDir)
|
|
122
|
+
const unitDir = join(buildDir, 'app')
|
|
123
|
+
await mkdir(unitDir, { recursive: true })
|
|
124
|
+
await writeFile(join(unitDir, 'bundle.js'), 'console.log("bundle")')
|
|
125
|
+
if (options.withFrontend) {
|
|
126
|
+
await mkdir(join(unitDir, 'frontend', 'assets'), { recursive: true })
|
|
127
|
+
await writeFile(
|
|
128
|
+
join(unitDir, 'frontend', 'index.html'),
|
|
129
|
+
'<!doctype html>'
|
|
130
|
+
)
|
|
131
|
+
await writeFile(join(unitDir, 'frontend', 'assets', 'app.js'), 'app')
|
|
132
|
+
await writeFile(
|
|
133
|
+
join(unitDir, 'frontend-assets.gen.js'),
|
|
134
|
+
'export const frontendAssets = {}\n'
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
return { buildDir, outDir: join(buildDir, 'app-dist') }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const silentLogger = { info: () => {}, error: () => {} }
|
|
141
|
+
|
|
142
|
+
test('ships the frontend beside the bundle', async () => {
|
|
143
|
+
// The node entry resolves its mount directory relative to itself, so the
|
|
144
|
+
// copy has to land in the distributable, not just in the build directory.
|
|
145
|
+
const { buildDir, outDir } = await builtUnit({ withFrontend: true })
|
|
146
|
+
|
|
147
|
+
const result = await new StandaloneProviderAdapter({
|
|
148
|
+
runtime: 'node',
|
|
149
|
+
}).deploy({ buildDir, logger: silentLogger })
|
|
150
|
+
|
|
151
|
+
assert.equal(result.success, true)
|
|
152
|
+
assert.equal(
|
|
153
|
+
await readFile(join(outDir, 'frontend', 'assets', 'app.js'), 'utf-8'),
|
|
154
|
+
'app'
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('ships the asset manifest the compile step still has to resolve', async () => {
|
|
159
|
+
// `bun build --compile` follows the import out of the copied bundle, so the
|
|
160
|
+
// manifest has to sit next to it — it was excluded from the esbuild output
|
|
161
|
+
// precisely so it would still be a real file at this point.
|
|
162
|
+
const { buildDir, outDir } = await builtUnit({ withFrontend: true })
|
|
163
|
+
|
|
164
|
+
await new StandaloneProviderAdapter({ runtime: 'node' }).deploy({
|
|
165
|
+
buildDir,
|
|
166
|
+
logger: silentLogger,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
assert.match(
|
|
170
|
+
await readFile(join(outDir, 'frontend-assets.gen.js'), 'utf-8'),
|
|
171
|
+
/frontendAssets/
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('a project without a frontend copies nothing extra', async () => {
|
|
176
|
+
const { buildDir, outDir } = await builtUnit({ withFrontend: false })
|
|
177
|
+
|
|
178
|
+
const result = await new StandaloneProviderAdapter({
|
|
179
|
+
runtime: 'node',
|
|
180
|
+
}).deploy({ buildDir, logger: silentLogger })
|
|
181
|
+
|
|
182
|
+
assert.equal(result.success, true)
|
|
183
|
+
assert.equal(existsSync(join(outDir, 'frontend')), false)
|
|
184
|
+
assert.equal(existsSync(join(outDir, 'frontend-assets.gen.js')), false)
|
|
185
|
+
})
|
|
186
|
+
})
|