@alxia/create 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -28,8 +28,8 @@ bun create @alxia my-site --template react-router
28
28
 
29
29
  | template | what it writes |
30
30
  | --- | --- |
31
- | `api` | an `@alxia/core` app with Zod: `POST /todos` validates its body, behind `requireKey`, a hook made with `defineHook` that answers 401 without an API key; a `bun test` spec calling it with `app.request()` and through `@alxia/client`, typed; `bun dev` restarting on change, `typecheck`, `build`, a strict `tsconfig.json`, a `Dockerfile` on `oven/bun:1`, `.dockerignore`, `.gitignore`, `.env.example` and a README |
32
- | `react-router` | React Router's official template, as `create-react-router` writes it, shipped in this package and copied, with [`@alxia/react-router`](https://www.npmjs.com/package/@alxia/react-router) added as its README says: `alxia()` in `vite.config.ts`'s plugins, `start` running `bun build/server/index.js`, a `bunfig.toml` starting React Router's CLI on Bun, and a `Dockerfile` on `oven/bun:1` in place of React Router's Node one. No server file: the default one serves the pages; `bunx alxia-react-router reveal` writes it out to customise |
31
+ | `api` | an `@alxia/core` app with Zod: `POST /todos` validates its body, behind `requireKey`, a hook made with `defineHook` that answers 401 without an API key; a `bun test` spec calling it with `app.request()` and through `@alxia/client`, typed; `bun dev` restarting on change, `typecheck`, `build`, a strict `tsconfig.json`, a `Dockerfile` running on `oven/bun:1-alpine`, `.dockerignore`, `.gitignore`, `.env.example` and a README |
32
+ | `react-router` | React Router's official template, as `create-react-router` writes it, shipped in this package and copied, with [`@alxia/react-router`](https://www.npmjs.com/package/@alxia/react-router) added as its README says: `alxia()` in `vite.config.ts`'s plugins, `start` running `bun build/server/index.js`, a `bunfig.toml` starting React Router's CLI on Bun, and a `Dockerfile` running on `oven/bun:1-alpine` in place of React Router's Node one. No server file: the default one serves the pages; `bunx alxia-react-router reveal` writes it out to customise |
33
33
 
34
34
  The heart of the `api` project, its route and hook (the whole file, with
35
35
  its imports and schemas, is in the [guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#the-api-template)):
@@ -53,8 +53,9 @@ export const app = alxia()
53
53
 
54
54
  ## Docker
55
55
 
56
- Both projects build into an image as they are written, on `oven/bun:1`,
57
- the image running the app as the non-root `bun` user. Each `Dockerfile`
56
+ Both projects build into an image as they are written: built on
57
+ `oven/bun:1`, run on `oven/bun:1-alpine` as the non-root `bun` user, an
58
+ image of about 130 MB. Each `Dockerfile`
58
59
  builds in a stage of its own, and the image holds the build output alone,
59
60
  no `node_modules`:
60
61
 
@@ -81,7 +82,9 @@ with `--frozen-lockfile`. The
81
82
  [guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#docker)
82
83
  has the stages, and
83
84
  [troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/create/docs/troubleshooting.md#error-cannot-find-package--from-appdistserverjs)
84
- what to do for a dependency that cannot be bundled.
85
+ what to do for a dependency that cannot be bundled, and for a
86
+ [native addon built for glibc alone](https://github.com/softistx/alxia/blob/develop/packages/create/docs/troubleshooting.md#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl),
87
+ which does not load on Alpine.
85
88
 
86
89
  ## Options
87
90
 
package/docs/guide.md CHANGED
@@ -67,10 +67,10 @@ my-api/
67
67
  ├── src/
68
68
  │ ├── app.ts the app, and its type
69
69
  │ ├── app.spec.ts bun test: app.request() and @alxia/client
70
- │ └── server.ts app.listen(PORT)
70
+ │ └── server.ts app.listen(PORT), stopped on SIGTERM
71
71
  ├── package.json
72
72
  ├── tsconfig.json
73
- ├── Dockerfile bun run build, then dist/ alone, on oven/bun:1
73
+ ├── Dockerfile bun run build, then dist/ alone, on oven/bun:1-alpine
74
74
  ├── .dockerignore
75
75
  ├── .env.example PORT and API_KEY, for a .env Bun loads
76
76
  ├── .gitignore
@@ -230,15 +230,25 @@ goes from there.
230
230
 
231
231
  ## Docker
232
232
 
233
- Each project's `Dockerfile` runs it on Bun, on the official `oven/bun:1`
234
- image, as the image's non-root `bun` user. The installs are
233
+ Each project's `Dockerfile` builds it on Bun's official `oven/bun:1`
234
+ image, Debian's, and runs it on `oven/bun:1-alpine`, as that image's
235
+ non-root `bun` user (uid 1000). The installs are
235
236
  `--frozen-lockfile`, from the `bun.lock` the command's `bun install`
236
237
  wrote: commit it.
237
238
 
238
239
  Each one builds in a stage of its own, and the image holds the build
239
240
  output alone: no `node_modules`, no sources. The dependencies are inside
240
241
  the bundle, so the image is the base image and a few hundred kilobytes to
241
- a few megabytes. A dependency that cannot be bundled is kept external and
242
+ a few megabytes: about 130 MB, where the same build on `oven/bun:1` is
243
+ about 345 MB.
244
+
245
+ The build stages stay on Debian: the tools a build runs, Vite's,
246
+ Tailwind's and any package's install script, are tried on glibc first,
247
+ and Alpine saves nothing there, since the stage is not shipped. What is
248
+ shipped is JavaScript, which Bun runs the same on musl. A native addon is
249
+ the exception: one built for glibc alone cannot load on Alpine, and the
250
+ final stage goes back to `oven/bun:1`
251
+ ([troubleshooting](troubleshooting.md#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl)). A dependency that cannot be bundled is kept external and
242
252
  copied in: see
243
253
  [troubleshooting](troubleshooting.md#error-cannot-find-package--from-appdistserverjs).
244
254
 
@@ -250,7 +260,8 @@ Two stages: every dependency, installed with
250
260
  running `bun --no-install dist/server.js`, `start`'s command with
251
261
  Bun's `--no-install` (not create-alxia's option of the same name), so that a package missing from the bundle fails at
252
262
  startup rather than being fetched from npm, written out so that Bun
253
- is the container's process.
263
+ is the container's process. `src/server.ts` stops the app on `SIGTERM`:
264
+ as process 1, Bun would otherwise ignore it, and `docker stop` would wait.
254
265
 
255
266
  ```dockerfile
256
267
  FROM oven/bun:1 AS build
@@ -260,7 +271,7 @@ RUN bun install --frozen-lockfile
260
271
  COPY . .
261
272
  RUN bun run build
262
273
 
263
- FROM oven/bun:1
274
+ FROM oven/bun:1-alpine
264
275
  WORKDIR /app
265
276
  ENV NODE_ENV=production
266
277
  COPY --from=build /app/dist ./dist
package/docs/roadmap.md CHANGED
@@ -30,6 +30,13 @@ Nothing scheduled yet.
30
30
 
31
31
  ### Next release
32
32
 
33
+ - **The images run on Alpine.** Every `Dockerfile`'s final stage is
34
+ `oven/bun:1-alpine`, the build stages staying on `oven/bun:1`: each
35
+ image is about 130 MB, where it was about 345 MB. The `api` project
36
+ stops on `SIGTERM`, so `docker stop` no longer waits.
37
+
38
+ ### 0.1.3
39
+
33
40
  - **Every `Dockerfile` builds, and the image holds the build alone.** The
34
41
  `api` template's builds `dist/server.js`, bundled, minified and source
35
42
  mapped, and runs it with no `node_modules` and no `src/`; `start` runs
@@ -37,6 +44,9 @@ Nothing scheduled yet.
37
44
  copies `build/` alone, which `@alxia/react-router`'s plugin now bundles
38
45
  whole. Each image is about 50 MB (api) and 150 MB (react-router)
39
46
  smaller.
47
+
48
+ ### 0.1.2
49
+
40
50
  - **The `api` template is files, copied, with a `Dockerfile`.** It ships
41
51
  under `templates/api/` and is copied as `react-router`'s is. New in it:
42
52
  a `Dockerfile` on `oven/bun:1`, `.dockerignore` and `.env.example`.
@@ -38,6 +38,7 @@ nothing — the symptom.
38
38
  - [`error: lockfile had changes, but lockfile is frozen`](#error-lockfile-had-changes-but-lockfile-is-frozen)
39
39
  - [`error: Module not found "dist/server.js"`](#error-module-not-found-distserverjs)
40
40
  - [`error: Cannot find package '…' from '/app/dist/server.js'`](#error-cannot-find-package--from-appdistserverjs)
41
+ - [`error: … is linked against glibc (DT_NEEDED libm.so.6), but this Bun build uses musl.`](#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl)
41
42
  - [The project's `@alxia/*` are older than npm's latest](#the-projects-alxia-are-older-than-npms-latest)
42
43
 
43
44
  ## Before it runs
@@ -325,6 +326,35 @@ COPY --from=production-dependencies /app/node_modules ./node_modules
325
326
  the bundle still imports: Node's and Bun's modules, and the packages left
326
327
  external.
327
328
 
329
+ ### `error: … is linked against glibc (DT_NEEDED libm.so.6), but this Bun build uses musl.`
330
+
331
+ The container stops at startup, or at the first request that loads the
332
+ package, with the path of a `.node` file in `node_modules` and
333
+ `code: "ERR_DLOPEN_FAILED"`. The library named after `DT_NEEDED` is
334
+ whichever one the addon links first: `libc.so.6`, `libstdc++.so.6` or
335
+ another; the fix is the same.
336
+
337
+ **When:** a package is kept external and installed beside the build, as
338
+ the previous entry says, and it loads a native addon built for glibc
339
+ alone.
340
+
341
+ **Why:** the final stage is `oven/bun:1-alpine`, whose C library is
342
+ musl. The bundle is JavaScript, which runs the same there, but a `.node`
343
+ file is compiled against one C library, and one compiled for glibc
344
+ cannot load on musl, even with `gcompat`. A package that publishes a
345
+ musl build too, as `sharp` does, works: `bun install`, `--production` too,
346
+ puts both variants in `node_modules`, and the package picks musl's.
347
+
348
+ **Fix:** run the final stage on Debian's image, `oven/bun:1`, which
349
+ holds glibc. The build stages stay as they are:
350
+
351
+ ```dockerfile
352
+ # The final stage.
353
+ FROM oven/bun:1
354
+ ```
355
+
356
+ The image is about 200 MB larger.
357
+
328
358
  ### The project's `@alxia/*` are older than npm's latest
329
359
 
330
360
  **Symptom:** a fresh project declares `@alxia/core` at `^0.3.4` while npm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/create",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Start an alxia app: `bun create @alxia` writes an API with Zod, or React Router's official template served by alxia",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,8 +14,10 @@ RUN bun run build
14
14
  # command, written out so that Bun is the container's process, and
15
15
  # --no-install: with no node_modules, Bun would otherwise fetch a package
16
16
  # the build left out from the registry at startup, where this fails. The
17
- # server listens on PORT (3000); set API_KEY.
18
- FROM oven/bun:1
17
+ # server listens on PORT (3000); set API_KEY. On Alpine, about 200 MB
18
+ # lighter: the bundle is JavaScript, which runs the same on musl. A native
19
+ # addon built for glibc alone needs oven/bun:1 here.
20
+ FROM oven/bun:1-alpine
19
21
  WORKDIR /app
20
22
  ENV NODE_ENV=production
21
23
  COPY --from=build /app/dist ./dist
@@ -60,13 +60,16 @@ beside `dist/`.
60
60
 
61
61
  ## Docker
62
62
 
63
- The `Dockerfile` builds in a stage of its own, on `oven/bun:1`: it
63
+ The `Dockerfile` builds in a stage of its own, on `oven/bun:1`, and runs
64
+ on `oven/bun:1-alpine`. The build stage
64
65
  installs every dependency with `--frozen-lockfile`, from the `bun.lock`
65
66
  that `bun install` wrote (commit it), and runs `bun run build`. The image
66
67
  holds `dist/` alone, no `node_modules` and no `src/`, and runs
67
68
  `bun --no-install dist/server.js` as its non-root `bun` user: a package
68
69
  missing from the bundle fails at startup instead of being fetched from
69
- npm.
70
+ npm. `src/server.ts` stops the app on `SIGTERM`, so `docker stop` is
71
+ immediate. A native addon built for glibc alone does not load on
72
+ Alpine: put the final stage back on `oven/bun:1`.
70
73
 
71
74
  ```sh
72
75
  docker build -t my-api .
@@ -1,4 +1,18 @@
1
1
  import { app } from './app';
2
2
 
3
+ // Stop as the platform asks. In a container Bun is process 1, which a
4
+ // signal with no handler does not stop: `docker stop` would wait.
5
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
6
+ process.once(signal, () => {
7
+ void app.stop().then(
8
+ () => process.exit(0),
9
+ (error: unknown) => {
10
+ console.error(error);
11
+ process.exit(1);
12
+ },
13
+ );
14
+ });
15
+ }
16
+
3
17
  const server = app.listen(Number(Bun.env['PORT'] ?? 3000));
4
18
  console.log(`listening on ${server.url}`);
@@ -16,8 +16,10 @@ RUN bun run build
16
16
  # command, and --no-install: with no node_modules, Bun would otherwise
17
17
  # fetch a package the build left out from the registry at startup,
18
18
  # where this fails.
19
- # The server listens on PORT (3000) and HOST (0.0.0.0).
20
- FROM oven/bun:1
19
+ # The server listens on PORT (3000) and HOST (0.0.0.0). On Alpine, about
20
+ # 200 MB lighter: build/ is JavaScript, which runs the same on musl. A
21
+ # native addon built for glibc alone needs oven/bun:1 here.
22
+ FROM oven/bun:1-alpine
21
23
  WORKDIR /app
22
24
  ENV NODE_ENV=production
23
25
  COPY --from=build /app/build ./build