create-nodejs-fn 0.0.2 → 0.1.0
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 +44 -19
- package/dist/index.d.mts +16 -1
- package/dist/index.mjs +35 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -162,27 +162,33 @@ Visit `http://localhost:5173/clock` to see a dynamically generated image with th
|
|
|
162
162
|
|
|
163
163
|
## 🪄 The Black Magic Revealed
|
|
164
164
|
|
|
165
|
-
### 1️⃣
|
|
165
|
+
### 1️⃣ Extract `nodejsFn` Contents (Clip & Crop)
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
Detects exported functions and **auto-generates proxy functions with identical type signatures**.
|
|
167
|
+
The plugin uses `ts-morph` to **statically analyze** `*.container.ts` files and **extracts the function bodies** wrapped in `nodejsFn()`.
|
|
169
168
|
|
|
170
169
|
```typescript
|
|
171
170
|
// Your code (clock.container.ts)
|
|
172
171
|
export const renderClock = nodejsFn(async () => {
|
|
173
|
-
|
|
172
|
+
const canvas = createCanvas(600, 200);
|
|
173
|
+
// ... Node.js native processing
|
|
174
174
|
return pngDataUrl;
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
// 🧙
|
|
178
|
-
// →
|
|
179
|
-
// → Calls are routed to the container via RPC!
|
|
177
|
+
// 🧙 Plugin extracts the inner function from nodejsFn()
|
|
178
|
+
// → Only the function body is clipped out for the container!
|
|
180
179
|
```
|
|
181
180
|
|
|
182
|
-
### 2️⃣
|
|
181
|
+
### 2️⃣ Bundle & Build Docker Image
|
|
182
|
+
|
|
183
|
+
The extracted functions are **bundled with esbuild** and combined with an auto-generated **Dockerfile** to create a Docker image.
|
|
184
|
+
|
|
185
|
+
- Functions are bundled as a Cap'n Proto RPC server
|
|
186
|
+
- Native dependencies specified in `external` are auto-extracted to `package.json`
|
|
187
|
+
- Dockerfile is auto-generated and image is built
|
|
183
188
|
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
### 3️⃣ Deploy as Cloudflare Containers
|
|
190
|
+
|
|
191
|
+
The generated Docker image is **bundled as Cloudflare Containers**, with **Durable Objects** managing the container lifecycle.
|
|
186
192
|
|
|
187
193
|
```typescript
|
|
188
194
|
// Route to specific instances with containerKey
|
|
@@ -195,11 +201,21 @@ export const renderClock = nodejsFn(
|
|
|
195
201
|
);
|
|
196
202
|
```
|
|
197
203
|
|
|
198
|
-
###
|
|
204
|
+
### 4️⃣ Auto-Replace Imports with Container Calls
|
|
199
205
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
Imports to `*.container.ts` files are **automatically replaced with proxy module imports** by the Vite plugin.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// Your code
|
|
210
|
+
import { renderClock } from "./clock.container";
|
|
211
|
+
|
|
212
|
+
// 🧙 Plugin auto-transforms this!
|
|
213
|
+
// → Actually imports a generated proxy function
|
|
214
|
+
// → Calls are transparently converted to Container RPC!
|
|
215
|
+
// → Types are fully preserved! IDE autocomplete works!
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Result**: Code that looks like normal function calls actually executes inside Docker containers!
|
|
203
219
|
|
|
204
220
|
|
|
205
221
|
## ⚙️ Plugin Options
|
|
@@ -236,6 +252,10 @@ createNodejsFnPlugin({
|
|
|
236
252
|
preInstallCommands: [],
|
|
237
253
|
postInstallCommands: [],
|
|
238
254
|
env: { MY_VAR: "value" },
|
|
255
|
+
// Run as a non-root user inside the container
|
|
256
|
+
user: { name: "app", uid: 1000, gid: 1000 },
|
|
257
|
+
// Or replace everything above with a fully custom Dockerfile
|
|
258
|
+
// dockerfilePath: "./containers/native.Dockerfile",
|
|
239
259
|
},
|
|
240
260
|
|
|
241
261
|
// Environment variables to pass from Worker to Container
|
|
@@ -249,6 +269,9 @@ createNodejsFnPlugin({
|
|
|
249
269
|
});
|
|
250
270
|
```
|
|
251
271
|
|
|
272
|
+
- `docker.user` lets you switch the runtime to a non-root user after installs while keeping generated paths (`/app`) writable.
|
|
273
|
+
- To own the entire build, supply `docker: { dockerfilePath: "./containers/native.Dockerfile" }`. The type prevents mixing this with other docker options so you don't accidentally combine incompatible settings. If the custom Dockerfile doesn't already start `server.mjs`, the generator will append `CMD ["node", "./server.mjs"]` to the end.
|
|
274
|
+
|
|
252
275
|
---
|
|
253
276
|
|
|
254
277
|
## 🏗️ Internal Architecture
|
|
@@ -259,11 +282,13 @@ project/
|
|
|
259
282
|
│ ├── clock.container.ts # Your code
|
|
260
283
|
│ ├── index.ts # Worker entry
|
|
261
284
|
│ └── __generated__/ # 🧙 Auto-generated magic
|
|
262
|
-
│ ├── create-nodejs-fn.ts
|
|
263
|
-
│ ├── create-nodejs-fn.do.ts
|
|
264
|
-
│ ├── create-nodejs-fn.context.ts
|
|
265
|
-
│ ├── create-nodejs-fn.runtime.ts
|
|
266
|
-
│
|
|
285
|
+
│ ├── create-nodejs-fn.ts # RPC client & type definitions
|
|
286
|
+
│ ├── create-nodejs-fn.do.ts # Durable Object class
|
|
287
|
+
│ ├── create-nodejs-fn.context.ts # Container key resolution
|
|
288
|
+
│ ├── create-nodejs-fn.runtime.ts # nodejsFn / containerKey helpers
|
|
289
|
+
│ ├── create-nodejs-fn-stub-batch.ts # Cap'n Proto RPC batch client
|
|
290
|
+
│ └── __proxies__/
|
|
291
|
+
│ └── p-XXXXXXXX.ts # Proxy functions (hashed)
|
|
267
292
|
│
|
|
268
293
|
└── .create-nodejs-fn/ # 🐳 Container build artifacts
|
|
269
294
|
├── Dockerfile # Auto-generated
|
package/dist/index.d.mts
CHANGED
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
|
|
3
|
-
type
|
|
3
|
+
type DockerUser = {
|
|
4
|
+
name: string;
|
|
5
|
+
uid?: number;
|
|
6
|
+
gid?: number;
|
|
7
|
+
};
|
|
8
|
+
type GeneratedDockerOptions = {
|
|
4
9
|
baseImage?: string;
|
|
5
10
|
systemPackages?: string[];
|
|
6
11
|
preInstallCommands?: string[];
|
|
7
12
|
postInstallCommands?: string[];
|
|
8
13
|
env?: Record<string, string>;
|
|
9
14
|
extraLines?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Create and switch to a non-root runtime user.
|
|
17
|
+
* Package installs still run as root; the user is applied before CMD.
|
|
18
|
+
*/
|
|
19
|
+
user?: DockerUser;
|
|
20
|
+
};
|
|
21
|
+
type CustomDockerfileOption = {
|
|
22
|
+
/** Use a fully custom Dockerfile (path resolved from project root). */
|
|
23
|
+
dockerfilePath: string;
|
|
10
24
|
};
|
|
25
|
+
type DockerOptions = GeneratedDockerOptions | CustomDockerfileOption;
|
|
11
26
|
type Opts = {
|
|
12
27
|
files?: string[];
|
|
13
28
|
generatedDir?: string;
|
package/dist/index.mjs
CHANGED
|
@@ -63,6 +63,9 @@ function collectExternalDeps(rootPkgPath, needed) {
|
|
|
63
63
|
dependencies: out
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
+
function isCustomDockerfile(opts) {
|
|
67
|
+
return Boolean(opts && "dockerfilePath" in opts && typeof opts.dockerfilePath === "string");
|
|
68
|
+
}
|
|
66
69
|
async function buildContainerServer(opts) {
|
|
67
70
|
const { mods, outBaseAbs, dockerOpts, containerPort, external, root } = opts;
|
|
68
71
|
ensureDir(outBaseAbs);
|
|
@@ -139,12 +142,35 @@ http.createServer((req, res) => {
|
|
|
139
142
|
`
|
|
140
143
|
);
|
|
141
144
|
}
|
|
145
|
+
if (isCustomDockerfile(dockerOpts)) {
|
|
146
|
+
const customPath = path.resolve(root, dockerOpts.dockerfilePath);
|
|
147
|
+
if (!fs.existsSync(customPath)) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`[create-nodejs-fn] Custom Dockerfile not found: ${dockerOpts.dockerfilePath} (resolved to ${customPath})`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
let customDockerfile = fs.readFileSync(customPath, "utf8");
|
|
153
|
+
if (!/server\.mjs/.test(customDockerfile) || !/\b(CMD|ENTRYPOINT)\b/.test(customDockerfile)) {
|
|
154
|
+
const trimmed = customDockerfile.replace(/\s*$/, "");
|
|
155
|
+
const suffix = [
|
|
156
|
+
trimmed,
|
|
157
|
+
"",
|
|
158
|
+
"# create-nodejs-fn runtime start",
|
|
159
|
+
'CMD ["node", "./server.mjs"]',
|
|
160
|
+
""
|
|
161
|
+
].join("\n");
|
|
162
|
+
customDockerfile = suffix;
|
|
163
|
+
}
|
|
164
|
+
writeFileIfChanged(dockerfile, customDockerfile);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
142
167
|
const {
|
|
143
168
|
baseImage = "node:20-slim",
|
|
144
169
|
systemPackages = [],
|
|
145
170
|
preInstallCommands = [],
|
|
146
171
|
postInstallCommands = [],
|
|
147
|
-
env: dockerEnv = {}
|
|
172
|
+
env: dockerEnv = {},
|
|
173
|
+
user
|
|
148
174
|
} = dockerOpts ?? {};
|
|
149
175
|
const installLines = "RUN corepack enable && pnpm install --prod --no-frozen-lockfile";
|
|
150
176
|
const sysDeps = systemPackages.length > 0 ? [
|
|
@@ -158,6 +184,13 @@ http.createServer((req, res) => {
|
|
|
158
184
|
"ENV NODE_ENV=production",
|
|
159
185
|
...Object.entries(dockerEnv).map(([k, v]) => `ENV ${k}=${JSON.stringify(v ?? "")}`)
|
|
160
186
|
];
|
|
187
|
+
const userLines = user && user.name ? [
|
|
188
|
+
"# Runtime user (from plugin options)",
|
|
189
|
+
`RUN groupadd --system${user.gid ? ` --gid ${user.gid}` : ""} ${user.name} \\`,
|
|
190
|
+
` && useradd --system --create-home --no-log-init --home-dir /home/${user.name} --gid ${user.name}${user.uid ? ` --uid ${user.uid}` : ""} ${user.name}`,
|
|
191
|
+
`RUN mkdir -p /app && chown -R ${user.name}:${user.name} /app`,
|
|
192
|
+
`USER ${user.name}`
|
|
193
|
+
] : [];
|
|
161
194
|
writeFileIfChanged(
|
|
162
195
|
dockerfile,
|
|
163
196
|
[
|
|
@@ -173,6 +206,7 @@ http.createServer((req, res) => {
|
|
|
173
206
|
"COPY ./server.mjs ./server.mjs",
|
|
174
207
|
...envLines,
|
|
175
208
|
...postRuns,
|
|
209
|
+
...userLines,
|
|
176
210
|
`EXPOSE ${containerPort}`,
|
|
177
211
|
`CMD ["node", "./server.mjs"]`,
|
|
178
212
|
""
|
package/package.json
CHANGED