@transclude/core 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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/bin/build.js +469 -0
  4. package/bin/check.js +78 -0
  5. package/bin/dev.js +348 -0
  6. package/bin/release.js +176 -0
  7. package/bin/serve.bun.js +15 -0
  8. package/bin/serve.deno.js +15 -0
  9. package/bin/serve.js +12 -0
  10. package/editor/server.js +172 -0
  11. package/editor/vscode/extension.js +49 -0
  12. package/editor/vscode/package.json +32 -0
  13. package/editor/vscode/syntaxes/transclude.injection.json +41 -0
  14. package/package.json +82 -0
  15. package/src/address.js +183 -0
  16. package/src/app.js +492 -0
  17. package/src/cache.js +137 -0
  18. package/src/compiler/bind.js +496 -0
  19. package/src/compiler/codegen.js +1061 -0
  20. package/src/compiler/expr.js +221 -0
  21. package/src/compiler/index.js +964 -0
  22. package/src/compiler/interp.js +82 -0
  23. package/src/compiler/script.js +620 -0
  24. package/src/compiler/shim.js +756 -0
  25. package/src/compiler/sourcemap.js +140 -0
  26. package/src/compiler/types.js +163 -0
  27. package/src/compress.js +104 -0
  28. package/src/cookies.js +157 -0
  29. package/src/csp.js +192 -0
  30. package/src/document.js +604 -0
  31. package/src/extract.js +339 -0
  32. package/src/feed.js +194 -0
  33. package/src/include.js +89 -0
  34. package/src/lookup.js +49 -0
  35. package/src/negotiate.js +95 -0
  36. package/src/plugin.js +423 -0
  37. package/src/pool.js +29 -0
  38. package/src/precache.js +68 -0
  39. package/src/production.js +159 -0
  40. package/src/project.js +110 -0
  41. package/src/proxy.js +319 -0
  42. package/src/public-files.js +77 -0
  43. package/src/rewrite.js +281 -0
  44. package/src/routes.js +199 -0
  45. package/src/runtime/index.js +1345 -0
  46. package/src/server.js +183 -0
  47. package/src/sitemap.js +124 -0
  48. package/src/static-cache.js +170 -0
  49. package/src/typecheck.js +492 -0
  50. package/src/worker.js +87 -0
package/bin/dev.js ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ // Hono routes; Vite compiles. The route table is the directory tree. See
3
+ // src/routes.js for the rules.
4
+
5
+ import fs from 'node:fs';
6
+ import http from 'node:http';
7
+ import path from 'node:path';
8
+ import { getRequestListener } from '@hono/node-server';
9
+ import { publicFiles as publicHandler } from '../src/public-files.js';
10
+ import { createServer as createViteServer } from 'vite';
11
+ import {
12
+ ACTION_METHODS,
13
+ hasRegion,
14
+ absoluteFrom,
15
+ methodsOf,
16
+ renderFragment,
17
+ renderRoute,
18
+ responseOf,
19
+ runAction,
20
+ withEnvelope,
21
+ } from '../src/document.js';
22
+ import { clientEntryUrl, pageModuleId } from '../src/plugin.js';
23
+ import { resolveRoutesDir, scanRoutes } from '../src/routes.js';
24
+ import { baseApp, endpointMethods, runEndpoint, SERVER_FILE } from '../src/server.js';
25
+ import { randomBytes } from 'node:crypto';
26
+ import { cookiesOf } from '../src/cookies.js';
27
+ import { loadProject, portOf } from '../src/project.js';
28
+ import { includeContext } from '../src/include.js';
29
+ import { nodeLookup } from '../src/lookup.js';
30
+
31
+ const { root, config } = await loadProject();
32
+ const routesDir = resolveRoutesDir(path.join(root, config.appDir), config.routesDir);
33
+ const PORT = portOf(config, process.env.PORT);
34
+
35
+ // Built the same way the production server and the build build theirs. Dev used
36
+ // to get only half of it, so a route include worked in production and threw
37
+ // here.
38
+ let include = null;
39
+
40
+ /**
41
+ * A signing secret for this process only, when the config has none.
42
+ *
43
+ * Dev, and dev alone. Signed cookies stop working across a restart, which is a
44
+ * fair price for a fresh clone that runs. Production does *not* do this: a server
45
+ * that invents a secret invalidates every session whenever it restarts and shares
46
+ * none with a second instance, and finding that out in production is worse than
47
+ * being told at the first signed cookie.
48
+ */
49
+ const cookieSecret = config.cookieSecret ?? randomBytes(32).toString('hex');
50
+ if (!config.cookieSecret) {
51
+ console.log('[transclude] no cookieSecret, so signing with a random one for this process');
52
+ }
53
+
54
+ const publicRoot = config.publicDir
55
+ ? path.join(root, config.appDir, config.publicDir)
56
+ : null;
57
+
58
+ // The same handler production mounts, over the source directory rather than the
59
+ // copy in dist. Not `precompressed`: nothing has written a .br next to these yet.
60
+ const publicFiles =
61
+ publicRoot && fs.existsSync(publicRoot)
62
+ ? publicHandler(path.relative(process.cwd(), publicRoot) || '.')
63
+ : null;
64
+
65
+ // Built before Vite, because Vite needs it: in middleware mode with no `hmr`
66
+ // option Vite starts its own WebSocket server on another port, the browser
67
+ // refuses that socket as cross-origin, and every edit needs a manual reload.
68
+ // Handing it this server puts the socket on the same origin as the page.
69
+ const server = http.createServer();
70
+
71
+ const vite = await createViteServer({
72
+ root,
73
+ appType: 'custom',
74
+ server: { middlewareMode: true, hmr: { server } },
75
+ // Vite would serve these itself, ahead of Hono, and production would serve
76
+ // them a different way, which is how dev and production come to disagree. One
77
+ // way instead: `baseApp` mounts Hono's static middleware in both.
78
+ publicDir: false,
79
+ });
80
+
81
+ /**
82
+ * What every loader and action is handed. `request` is the platform's own
83
+ * `Request` rather than the server's wrapper. The framework should not be the
84
+ * reason an author has to learn a router's API to read a form.
85
+ */
86
+ const contextFor = (route, c, extra = {}) => ({
87
+ url: c.req.url,
88
+ // Only ask Hono for params when the route declares them: on the not-found
89
+ // path nothing matched, and c.req.param() has no stash to read.
90
+ params: route.params.length ? c.req.param() : {},
91
+ route: { id: route.id, pattern: route.pattern, path: c.req.path },
92
+ request: c.req.raw,
93
+ // The region this request asked for, or null for a whole document. An action
94
+ // needs it to decide whether a redirect is even an answer: post/redirect/get
95
+ // is right for a form, and wrong for a caller that asked for markup.
96
+ fragment: fragmentOf(c),
97
+ action: null,
98
+ ...withResponse(c, extra),
99
+ });
100
+
101
+ /**
102
+ * The envelope and the cookies that write into it, built together. The cookie
103
+ * helpers hold the same `response` the server will read, so a `set` in a loader
104
+ * lands on the way out.
105
+ */
106
+ function withResponse(c, extra) {
107
+ const response = responseOf();
108
+ return {
109
+ response,
110
+ cookies: cookiesOf(c.req.raw, response, cookieSecret),
111
+ absolute: absoluteFrom(config.metadataBase, c.req.url),
112
+ ...extra,
113
+ };
114
+ }
115
+
116
+ /** Whatever the loaders and actions put on `ctx.response`, on the way out. */
117
+ const sendWith = (c, { response }, html, status) => {
118
+ for (const [name, value] of response.headers) c.header(name, value);
119
+ return c.html(html, status ?? response.status);
120
+ };
121
+
122
+ const renderPage = async (route, c, status = null, extra = {}) => {
123
+ const page = await vite.ssrLoadModule(pageModuleId(route.id));
124
+ const ctx = contextFor(route, c, extra);
125
+
126
+ const html = await renderRoute(page, ctx, {
127
+ clientEntry: page.client.needed ? clientEntryUrl(route.id) : null,
128
+ // No query param: Vite content-negotiates, and a <link> sends
129
+ // `Accept: text/css`, so the plain path returns the stylesheet.
130
+ stylesheet: config.stylesheet ? `/${config.stylesheet}` : null,
131
+ csp: config.csp,
132
+ lang: config.lang,
133
+ include,
134
+ });
135
+ // A loader answered for itself: a redirect, or something that is not a page.
136
+ if (html instanceof Response) return withEnvelope(html, ctx);
137
+
138
+ return sendWith(c, ctx, await vite.transformIndexHtml(c.req.path, html), status);
139
+ };
140
+
141
+ /**
142
+ * The region this request is asking for, or null for the whole document. An
143
+ * empty value (`?fragment`) is the page's own body without its layouts.
144
+ */
145
+ const fragmentOf = (c) => {
146
+ if (!config.fragmentParam) return null;
147
+ const value = c.req.query(config.fragmentParam);
148
+ return value === undefined ? null : value;
149
+ };
150
+
151
+ const sendFragment = async (route, c, region, extra = {}) => {
152
+ const page = await vite.ssrLoadModule(pageModuleId(route.id));
153
+ const ctx = contextFor(route, c, extra);
154
+ const html = await renderFragment(page, ctx, { region: region || null, include });
155
+
156
+ if (html instanceof Response) return withEnvelope(html, ctx);
157
+ if (html === null) return c.text(`no fragment "${region}" on ${route.rel}`, 404);
158
+ // No Vite transform: a fragment is inserted into a document that already ran
159
+ // the client entry, and injecting the HMR preamble again would run it twice.
160
+ return sendWith(c, ctx, html);
161
+ };
162
+
163
+ /**
164
+ * A form submission, or anything else that is not a GET.
165
+ *
166
+ * The action runs first, then the request is answered the same way a GET is: the
167
+ * whole document, or one region if the URL asked for one. That last part is what
168
+ * regions are for. POST a form to `?fragment=list` and what comes back is
169
+ * the list, already rendered, by the same compiled region the page uses.
170
+ */
171
+ const handleAction = async (route, c) => {
172
+ const page = await vite.ssrLoadModule(pageModuleId(route.id));
173
+ const region = fragmentOf(c);
174
+
175
+ // Before the action, not after: a request nobody can answer should not have
176
+ // mutated anything on its way to saying so.
177
+ if (region !== null && !hasRegion(page, region)) {
178
+ return c.text(`no fragment "${region}" on ${route.rel}`, 404);
179
+ }
180
+
181
+ const ctx = contextFor(route, c);
182
+ const outcome = await runAction(page, ctx, c.req.method);
183
+
184
+ if (!outcome) {
185
+ return c.text(`${c.req.method} not allowed on ${route.rel}`, 405, {
186
+ Allow: methodsOf(page).join(', '),
187
+ });
188
+ }
189
+ if (outcome.response) return withEnvelope(outcome.response, ctx);
190
+
191
+ const extra = { action: outcome.action, cookies: ctx.cookies, response: ctx.response };
192
+ return region === null
193
+ ? renderPage(route, c, 200, extra)
194
+ : sendFragment(route, c, region, extra);
195
+ };
196
+
197
+ const onError = (c, err) => {
198
+ // Before anything reads the stack: Vite's transform means the raw one points
199
+ // at generated code, and a reporter given that is worse than none.
200
+ vite.ssrFixStacktrace(err);
201
+ console.error(err);
202
+
203
+ // The same seam production has, so a reporter is exercised while you are the
204
+ // one looking at it rather than first on a live site.
205
+ if (typeof config.onError === 'function') {
206
+ try {
207
+ config.onError(err, { request: c.req.raw, url: c.req.url, method: c.req.method });
208
+ } catch (failed) {
209
+ console.error('[transclude] onError itself threw:', failed);
210
+ }
211
+ }
212
+
213
+ return c.text(`${err.name}: ${err.message}\n\n${err.stack ?? ''}`, 500);
214
+ };
215
+
216
+ const serverFile = path.join(root, config.appDir, SERVER_FILE);
217
+
218
+ /**
219
+ * The app's own middleware, through Vite so an edit to it is picked up the same
220
+ * way an edit to a page is. Production gets the same module out of the SSR
221
+ * bundle instead, because that server reads `dist` and nothing else.
222
+ *
223
+ * Invalidated first, and not for tidiness: the watcher below and Vite's own
224
+ * invalidation are separate handlers on the same event, and if this runs before
225
+ * Vite's, `ssrLoadModule` hands back the module as it was. Measured: the first
226
+ * edit was ignored and the second appeared to work.
227
+ */
228
+ async function loadMiddleware() {
229
+ if (!fs.existsSync(serverFile)) return null;
230
+
231
+ const url = `/${config.appDir}/${SERVER_FILE}`;
232
+ const node = await vite.moduleGraph.getModuleByUrl(url, true);
233
+ if (node) vite.moduleGraph.invalidateModule(node);
234
+
235
+ const mod = await vite.ssrLoadModule(url);
236
+ return mod.default ?? null;
237
+ }
238
+
239
+ async function buildApp() {
240
+ const { routes, endpoints, notFound } = scanRoutes(routesDir);
241
+
242
+ include = includeContext({
243
+ config,
244
+ routes,
245
+ pageFor: (id) => vite.ssrLoadModule(pageModuleId(id)),
246
+ lookup: nodeLookup(),
247
+ });
248
+ const app = baseApp({
249
+ csrf: config.csrf,
250
+ trailingSlash: config.trailingSlash,
251
+ publicFiles,
252
+ middleware: await loadMiddleware(),
253
+ });
254
+
255
+ // Already ordered most-specific first, so registration order is deterministic
256
+ // rather than something to reason about per-router.
257
+ for (const route of routes) {
258
+ app.get(route.pattern, async (c) => {
259
+ try {
260
+ const fragment = fragmentOf(c);
261
+ return fragment === null ? await renderPage(route, c) : await sendFragment(route, c, fragment);
262
+ } catch (err) {
263
+ return onError(c, err);
264
+ }
265
+ });
266
+
267
+ // Registered for every route rather than only the ones with an action, so
268
+ // a POST to a page that has none is a 405 with an `Allow` header instead of
269
+ // the not-found page. The URL exists, the method does not.
270
+ app.on(ACTION_METHODS, route.pattern, async (c) => {
271
+ try {
272
+ return await handleAction(route, c);
273
+ } catch (err) {
274
+ return onError(c, err);
275
+ }
276
+ });
277
+ }
278
+
279
+ // `app.all`, because "every verb" is the point: the module decides which it
280
+ // answers, and anything it does not is a 405 rather than a 404.
281
+ for (const route of endpoints) {
282
+ const url = '/' + path.relative(root, route.file).split(path.sep).join('/');
283
+ app.all(route.pattern, async (c) => {
284
+ try {
285
+ const mod = await vite.ssrLoadModule(url);
286
+ // The same envelope every other path gets. An endpoint that sets a
287
+ // cookie and returns a redirect is an ordinary thing to write, and the
288
+ // `Set-Cookie` was dropped without it.
289
+ const ctx = contextFor(route, c);
290
+ const out = await runEndpoint(mod, ctx, c.req.method);
291
+ if (out) return withEnvelope(out, ctx);
292
+ return c.text(`${c.req.method} not allowed on ${route.rel}`, 405, {
293
+ Allow: endpointMethods(mod).join(', '),
294
+ });
295
+ } catch (err) {
296
+ return onError(c, err);
297
+ }
298
+ });
299
+ }
300
+
301
+ app.notFound(async (c) => {
302
+ if (!notFound) return c.text('not found', 404);
303
+ try {
304
+ return await renderPage(notFound, c, 404);
305
+ } catch (err) {
306
+ return onError(c, err);
307
+ }
308
+ });
309
+
310
+ console.log(
311
+ `[routes]\n${[...routes, ...endpoints].map((r) => ` ${r.pattern.padEnd(24)} ${r.rel}`).join('\n')}` +
312
+ (notFound ? `\n ${'(not found)'.padEnd(24)} ${notFound.rel}` : ''),
313
+ );
314
+
315
+ return app;
316
+ }
317
+
318
+ let app = await buildApp();
319
+
320
+ // Adding or removing a page changes the route table, not just a module.
321
+ vite.watcher.on('all', async (event, file) => {
322
+ // `.js` as well as `.html`: an endpoint is a route too, and watching only for
323
+ // pages meant adding one needed a restart, with a 404 as the only hint.
324
+ const extension = path.extname(file);
325
+ const routing =
326
+ file.startsWith(routesDir) &&
327
+ (extension === '.html' || extension === '.js') &&
328
+ event !== 'change';
329
+ // Middleware is registered once when the app is built, so a change to it needs
330
+ // the app rebuilt, unlike a page, which is loaded per request.
331
+ if (!routing && file !== serverFile) return;
332
+ try {
333
+ app = await buildApp();
334
+ } catch (err) {
335
+ console.error(err.message);
336
+ }
337
+ });
338
+
339
+ const hono = getRequestListener((request) => app.fetch(request));
340
+
341
+ // Vite owns /@id/, /@vite/client and /src/*; Hono gets everything else.
342
+ server.on('request', (req, res) => {
343
+ vite.middlewares(req, res, () => hono(req, res));
344
+ });
345
+
346
+ server.listen(PORT, () => {
347
+ console.log(`http://localhost:${PORT}`);
348
+ });
package/bin/release.js ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // Prepares a release. It does not publish: it sets the version in both packages,
3
+ // runs everything, and writes a tag. Pushing that tag is what publishes, and
4
+ // that happens in CI where the token lives and provenance can be signed.
5
+ //
6
+ // Both packages move together and share a version. They are one project split
7
+ // for a packaging reason, and a new project's dependency on `@transclude/core`
8
+ // is written by `@transclude/create` from its own version, so two numbers that
9
+ // drift would write a dependency that does not exist.
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { execFileSync } from 'node:child_process';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
17
+ const MANIFESTS = ['package.json', 'create/package.json'];
18
+
19
+ const run = (command, args, options = {}) =>
20
+ execFileSync(command, args, { cwd: root, encoding: 'utf8', stdio: 'pipe', ...options });
21
+
22
+ const read = (rel) => JSON.parse(fs.readFileSync(path.join(root, rel), 'utf8'));
23
+
24
+ function usage() {
25
+ return [
26
+ 'Usage: node bin/release.js <version|major|minor|patch> [--dry-run]',
27
+ '',
28
+ ' Sets the version in both packages, verifies, commits and tags.',
29
+ ' Pushing the tag is what publishes. Nothing here talks to a registry.',
30
+ '',
31
+ ].join('\n');
32
+ }
33
+
34
+ /** `1.2.3`, or what `major`/`minor`/`patch` makes of the current one. */
35
+ function nextVersion(current, asked) {
36
+ if (/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(asked)) return asked;
37
+
38
+ const [major, minor, patch] = current.split('.').map(Number);
39
+ if (asked === 'major') return `${major + 1}.0.0`;
40
+ if (asked === 'minor') return `${major}.${minor + 1}.0`;
41
+ if (asked === 'patch') return `${major}.${minor}.${patch + 1}`;
42
+
43
+ throw new Error(`${JSON.stringify(asked)} is not a version or major/minor/patch`);
44
+ }
45
+
46
+ /**
47
+ * Refuses to release from a tree that is not exactly what was tested.
48
+ *
49
+ * A dirty tree means the tag would name a commit that does not hold what was
50
+ * verified, and the tarball CI builds comes from the tag rather than from here.
51
+ */
52
+ function assertReleasable() {
53
+ if (run('git', ['status', '--porcelain']).trim()) {
54
+ throw new Error('the working tree has changes. Commit or stash them first.');
55
+ }
56
+
57
+ const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
58
+ if (branch !== 'main') throw new Error(`on ${branch}, and a release is cut from main`);
59
+
60
+ return read('package.json').version;
61
+ }
62
+
63
+ /**
64
+ * Refuses a version that already has a tag.
65
+ *
66
+ * Re-tagging is how two different tarballs come to claim one version: the
67
+ * registry keeps the first and the tag names the second, and nothing afterwards
68
+ * says which one anybody installed.
69
+ */
70
+ function assertUntagged(version) {
71
+ const tags = run('git', ['tag', '--list']).split('\n').map((t) => t.trim());
72
+ if (tags.includes(`v${version}`)) {
73
+ throw new Error(`v${version} is already tagged. Releasing it again would move the tag.`);
74
+ }
75
+ }
76
+
77
+ function setVersion(version) {
78
+ for (const rel of MANIFESTS) {
79
+ const file = path.join(root, rel);
80
+ const manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
81
+ manifest.version = version;
82
+ fs.writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
83
+ }
84
+ }
85
+
86
+ /** Everything, in the order that fails cheapest first. */
87
+ function verify() {
88
+ const steps = [
89
+ // `check:src` is not here. It exits non-zero on any diagnostic, and most of
90
+ // what it reports is a pattern TypeScript cannot model rather than a defect.
91
+ // The part that is a gate is a test, and `npm test` runs it.
92
+ ['the framework', 'npm', ['test']],
93
+ ['the demo', 'npm', ['run', 'test:examples']],
94
+ ['the docs', 'npm', ['test', '--prefix', 'docs']],
95
+ ['the docs types', 'npm', ['run', 'check', '--prefix', 'docs']],
96
+ ['the docs build', 'npm', ['run', 'build', '--prefix', 'docs']],
97
+ ];
98
+
99
+ for (const [what, command, args] of steps) {
100
+ process.stdout.write(` ${what} … `);
101
+ try {
102
+ run(command, args);
103
+ process.stdout.write('ok\n');
104
+ } catch (error) {
105
+ process.stdout.write('failed\n\n');
106
+ throw new Error(`${what} failed:\n\n${error.stdout ?? ''}${error.stderr ?? ''}`);
107
+ }
108
+ }
109
+ }
110
+
111
+ /** What each package would ship, so a missing `files` entry is seen before a tag. */
112
+ function packed() {
113
+ for (const dir of [root, path.join(root, 'create')]) {
114
+ const listing = run('npm', ['pack', '--dry-run', '--json'], { cwd: dir });
115
+ const [{ name, files }] = JSON.parse(listing);
116
+ process.stdout.write(` ${name}: ${files.length} files\n`);
117
+ }
118
+ }
119
+
120
+ function main() {
121
+ const args = process.argv.slice(2);
122
+ const dryRun = args.includes('--dry-run');
123
+ const asked = args.find((a) => !a.startsWith('-'));
124
+
125
+ if (!asked || args.includes('--help')) {
126
+ process.stdout.write(usage());
127
+ return;
128
+ }
129
+
130
+ const current = assertReleasable();
131
+ const version = nextVersion(current, asked);
132
+ assertUntagged(version);
133
+ process.stdout.write(`\n${current} -> ${version}\n\n`);
134
+
135
+ setVersion(version);
136
+
137
+ try {
138
+ verify();
139
+ packed();
140
+ } catch (error) {
141
+ // Put the versions back. A failed release should leave nothing behind.
142
+ setVersion(current);
143
+ throw error;
144
+ }
145
+
146
+ if (dryRun) {
147
+ setVersion(current);
148
+ process.stdout.write('\nDry run. Versions put back, nothing committed.\n\n');
149
+ return;
150
+ }
151
+
152
+ run('git', ['add', ...MANIFESTS]);
153
+
154
+ // A first release at the version the manifests already carry changes nothing,
155
+ // and an empty commit is not worth making. The tag is the release either way.
156
+ const staged = run('git', ['diff', '--cached', '--name-only']).trim();
157
+ if (staged) run('git', ['commit', '-m', `Release ${version}`]);
158
+
159
+ run('git', ['tag', '-a', `v${version}`, '-m', `Release ${version}`]);
160
+
161
+ process.stdout.write(
162
+ [
163
+ `\nTagged v${version}. Nothing has been published yet.\n\n`,
164
+ ' git push --follow-tags\n\n',
165
+ 'That is what publishes. The workflow builds from the tag and signs\n',
166
+ 'provenance against it.\n\n',
167
+ ].join(''),
168
+ );
169
+ }
170
+
171
+ try {
172
+ main();
173
+ } catch (error) {
174
+ process.stderr.write(`\n${error.message}\n`);
175
+ process.exitCode = 1;
176
+ }
@@ -0,0 +1,15 @@
1
+ // Bun adapter. `bun framework/bin/serve.bun.js`
2
+ //
3
+ // Bun serves whatever a module default-exports with a `fetch`, so there is no
4
+ // listener to write. The app already is one.
5
+
6
+ import { app, noBuild, port, summary } from '../src/production.js';
7
+
8
+ if (noBuild) {
9
+ console.error('No build found. Run `npm run build` first.');
10
+ process.exit(1);
11
+ }
12
+
13
+ summary(port);
14
+
15
+ export default { fetch: app.fetch, port };
@@ -0,0 +1,15 @@
1
+ // Deno adapter. `deno run -A framework/bin/serve.deno.js`
2
+ //
3
+ // `Deno.serve` takes the same (Request) => Response the other two do.
4
+
5
+ import { app, noBuild, port as configured, summary } from '../src/production.js';
6
+
7
+ if (noBuild) {
8
+ console.error('No build found. Run `npm run build` first.');
9
+ Deno.exit(1);
10
+ }
11
+
12
+ // Deno reads its own environment, so PORT is applied here rather than
13
+ // through the `process` shim.
14
+ const port = Number(Deno.env.get('PORT') ?? configured);
15
+ Deno.serve({ port, onListen: () => summary(port) }, app.fetch);
package/bin/serve.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ // Node adapter. The app is in src/production.js; this listens with it.
3
+
4
+ import { serve } from '@hono/node-server';
5
+ import { app, noBuild, port, summary } from '../src/production.js';
6
+
7
+ if (noBuild) {
8
+ console.error('No build found. Run `npm run build` first.');
9
+ process.exit(1);
10
+ }
11
+
12
+ serve({ fetch: app.fetch, port }, ({ port }) => summary(port));