@b9g/shovel 0.2.8 → 0.2.10
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 +32 -0
- package/bin/cli.js +1 -1
- package/bin/create.js +334 -120
- package/package.json +4 -4
- package/src/_chunks/{develop-SVLFKAF5.js → develop-N265AMEN.js} +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Shovel will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.2.9] - 2026-02-20
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **Dev server Ctrl+O** - Open the dev server URL in the default browser with `Ctrl+O`
|
|
10
|
+
- **Dev server signal handling** - `Ctrl+Z` (suspend), `Ctrl+D` (quit), `Ctrl+\` (quit) now work correctly in raw mode instead of being silently swallowed
|
|
11
|
+
- **Dev server input passthrough** - Typing, Enter, and Backspace now echo to stdout instead of being dropped
|
|
12
|
+
|
|
13
|
+
### Dependencies
|
|
14
|
+
|
|
15
|
+
- **`@logtape/logtape`** `^1.2.0` → `^2.0.0` across all packages
|
|
16
|
+
- **`@logtape/file`** `^1.0.0` → `^2.0.0`
|
|
17
|
+
- **`@b9g/filesystem`** `0.2.0` - Version bump
|
|
18
|
+
- **`@b9g/filesystem-s3`** `0.2.0` - Version bump
|
|
19
|
+
|
|
20
|
+
### Documentation
|
|
21
|
+
|
|
22
|
+
- **`@b9g/filesystem` README rewritten** - All class names were fabricated in the previous README. Corrected to match actual exports: `MemoryDirectory`, `NodeFSDirectory`, `S3Directory`, `CustomDirectoryStorage`
|
|
23
|
+
- Fixed inaccurate config options in `@b9g/assets` README
|
|
24
|
+
- Fixed `PostMessageCache` constructor signature in `@b9g/cache` README
|
|
25
|
+
- Fixed import paths in `@b9g/oauth2` README (`@b9g/auth` → `@b9g/oauth2`)
|
|
26
|
+
|
|
27
|
+
## [0.2.8] - 2026-02-10
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
- **Config file watching during develop** - `shovel.json` and `package.json` changes are now correctly detected during `shovel develop`. The previous `fs.watch()` approach broke on atomic saves (most editors write to a temp file then rename). Now uses esbuild's native `watchFiles` mechanism which handles this correctly. ([#59](https://github.com/bikeshaving/shovel/issues/59))
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
- **Dev server keyboard shortcuts** - `Ctrl+R` (reload), `Ctrl+L` (clear), `Ctrl+C` (quit), `?` (help) shortcuts in the dev server terminal. Only active when stdin is a TTY. ([#60](https://github.com/bikeshaving/shovel/issues/60))
|
|
36
|
+
|
|
5
37
|
## [0.2.7] - 2026-02-06
|
|
6
38
|
|
|
7
39
|
### Features
|
package/bin/cli.js
CHANGED
|
@@ -75,7 +75,7 @@ program.command("develop <entrypoint>").description("Start development server wi
|
|
|
75
75
|
DEFAULTS.WORKERS
|
|
76
76
|
).option("--platform <name>", "Runtime platform (node, cloudflare, bun)").action(async (entrypoint, options) => {
|
|
77
77
|
checkPlatformReexec(options);
|
|
78
|
-
const { developCommand } = await import("../src/_chunks/develop-
|
|
78
|
+
const { developCommand } = await import("../src/_chunks/develop-N265AMEN.js");
|
|
79
79
|
await developCommand(entrypoint, options, config);
|
|
80
80
|
});
|
|
81
81
|
program.command("create [name]").description("Create a new Shovel project").action(async (name) => {
|
package/bin/create.js
CHANGED
|
@@ -26,10 +26,16 @@ function parseFlags(args) {
|
|
|
26
26
|
const arg = args[i];
|
|
27
27
|
if (arg === "--template" && args[i + 1])
|
|
28
28
|
flags.template = args[++i];
|
|
29
|
+
else if (arg === "--framework" && args[i + 1])
|
|
30
|
+
flags.framework = args[++i];
|
|
29
31
|
else if (arg === "--typescript")
|
|
30
32
|
flags.typescript = true;
|
|
31
33
|
else if (arg === "--no-typescript")
|
|
32
34
|
flags.typescript = false;
|
|
35
|
+
else if (arg === "--jsx")
|
|
36
|
+
flags.jsx = true;
|
|
37
|
+
else if (arg === "--no-jsx")
|
|
38
|
+
flags.jsx = false;
|
|
33
39
|
else if (arg === "--platform" && args[i + 1])
|
|
34
40
|
flags.platform = args[++i];
|
|
35
41
|
}
|
|
@@ -70,14 +76,11 @@ async function main() {
|
|
|
70
76
|
}
|
|
71
77
|
let template;
|
|
72
78
|
let uiFramework = "vanilla";
|
|
73
|
-
if (flags.template
|
|
74
|
-
template = "static-site";
|
|
75
|
-
uiFramework = "crank";
|
|
76
|
-
} else if (flags.template) {
|
|
79
|
+
if (flags.template) {
|
|
77
80
|
const valid = ["hello-world", "api", "static-site", "full-stack"];
|
|
78
81
|
if (!valid.includes(flags.template)) {
|
|
79
82
|
console.error(
|
|
80
|
-
`Error: Unknown template "${flags.template}". Valid options: ${valid.join(", ")}
|
|
83
|
+
`Error: Unknown template "${flags.template}". Valid options: ${valid.join(", ")}`
|
|
81
84
|
);
|
|
82
85
|
process.exit(1);
|
|
83
86
|
}
|
|
@@ -114,38 +117,65 @@ async function main() {
|
|
|
114
117
|
}
|
|
115
118
|
template = templateResult;
|
|
116
119
|
}
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
120
|
+
if (template === "static-site" || template === "full-stack") {
|
|
121
|
+
if (flags.framework) {
|
|
122
|
+
const valid = ["vanilla", "htmx", "alpine", "crank"];
|
|
123
|
+
if (!valid.includes(flags.framework)) {
|
|
124
|
+
console.error(
|
|
125
|
+
`Error: Unknown framework "${flags.framework}". Valid options: ${valid.join(", ")}`
|
|
126
|
+
);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
uiFramework = flags.framework;
|
|
130
|
+
} else {
|
|
131
|
+
const framework = await select({
|
|
132
|
+
message: "UI framework:",
|
|
133
|
+
initialValue: "crank",
|
|
134
|
+
options: [
|
|
135
|
+
{
|
|
136
|
+
value: "alpine",
|
|
137
|
+
label: "Alpine.js",
|
|
138
|
+
hint: "Lightweight reactivity with x-data directives"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
value: "crank",
|
|
142
|
+
label: "Crank.js",
|
|
143
|
+
hint: "JSX components rendered on the server"
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
value: "htmx",
|
|
147
|
+
label: "HTMX",
|
|
148
|
+
hint: "HTML-driven interactions with hx- attributes"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
value: "vanilla",
|
|
152
|
+
label: "Vanilla",
|
|
153
|
+
hint: "Plain HTML, no framework"
|
|
154
|
+
}
|
|
155
|
+
]
|
|
156
|
+
});
|
|
157
|
+
if (typeof framework === "symbol") {
|
|
158
|
+
outro("Project creation cancelled");
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
uiFramework = framework;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let useJSX = true;
|
|
165
|
+
if (uiFramework === "crank") {
|
|
166
|
+
if (flags.jsx !== void 0) {
|
|
167
|
+
useJSX = flags.jsx;
|
|
168
|
+
} else {
|
|
169
|
+
const jsxResult = await confirm({
|
|
170
|
+
message: "Use JSX?",
|
|
171
|
+
initialValue: true
|
|
172
|
+
});
|
|
173
|
+
if (typeof jsxResult === "symbol") {
|
|
174
|
+
outro("Project creation cancelled");
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
useJSX = jsxResult;
|
|
147
178
|
}
|
|
148
|
-
uiFramework = framework;
|
|
149
179
|
}
|
|
150
180
|
let typescript;
|
|
151
181
|
if (flags.typescript !== void 0) {
|
|
@@ -205,7 +235,8 @@ async function main() {
|
|
|
205
235
|
platform,
|
|
206
236
|
template,
|
|
207
237
|
typescript,
|
|
208
|
-
uiFramework
|
|
238
|
+
uiFramework,
|
|
239
|
+
useJSX
|
|
209
240
|
};
|
|
210
241
|
const s = spinner();
|
|
211
242
|
s.start("Creating your Shovel project...");
|
|
@@ -213,12 +244,13 @@ async function main() {
|
|
|
213
244
|
await createProject(config, projectPath);
|
|
214
245
|
s.stop("Project created");
|
|
215
246
|
console.info("");
|
|
216
|
-
outro("Your
|
|
247
|
+
outro("Your project is shovel-ready!");
|
|
248
|
+
const pm = platform === "bun" ? "bun" : "npm";
|
|
217
249
|
console.info("");
|
|
218
250
|
console.info("Next steps:");
|
|
219
251
|
console.info(` cd ${projectName}`);
|
|
220
|
-
console.info(`
|
|
221
|
-
console.info(`
|
|
252
|
+
console.info(` ${pm} install`);
|
|
253
|
+
console.info(` ${pm} run develop`);
|
|
222
254
|
console.info("");
|
|
223
255
|
console.info("Your app will be available at: http://localhost:7777");
|
|
224
256
|
console.info("");
|
|
@@ -231,37 +263,55 @@ async function main() {
|
|
|
231
263
|
async function createProject(config, projectPath) {
|
|
232
264
|
await mkdir(projectPath, { recursive: true });
|
|
233
265
|
await mkdir(join(projectPath, "src"), { recursive: true });
|
|
234
|
-
const ext = config.uiFramework === "crank" ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
266
|
+
const ext = config.uiFramework === "crank" && config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
267
|
+
const isCrank = config.uiFramework === "crank";
|
|
268
|
+
const entryFile = isCrank ? `src/server.${ext}` : `src/app.${ext}`;
|
|
235
269
|
const startCmd = config.platform === "bun" ? "bun dist/server/supervisor.js" : "node dist/server/supervisor.js";
|
|
236
270
|
const dependencies = {
|
|
237
271
|
"@b9g/router": "^0.2.0",
|
|
238
272
|
"@b9g/shovel": "^0.2.0"
|
|
239
273
|
};
|
|
240
|
-
if (
|
|
274
|
+
if (isCrank) {
|
|
241
275
|
dependencies["@b9g/crank"] = "^0.7.2";
|
|
242
276
|
}
|
|
277
|
+
const devDependencies = {};
|
|
278
|
+
if (config.typescript) {
|
|
279
|
+
devDependencies["@types/node"] = "^18.0.0";
|
|
280
|
+
devDependencies["typescript"] = "^5.0.0";
|
|
281
|
+
}
|
|
282
|
+
if (isCrank) {
|
|
283
|
+
devDependencies["eslint"] = "^9.0.0";
|
|
284
|
+
devDependencies["@eslint/js"] = "^9.0.0";
|
|
285
|
+
}
|
|
286
|
+
const scripts = {
|
|
287
|
+
develop: `shovel develop ${entryFile} --platform ${config.platform}`,
|
|
288
|
+
build: `shovel build ${entryFile} --platform ${config.platform}`,
|
|
289
|
+
start: startCmd
|
|
290
|
+
};
|
|
291
|
+
if (isCrank) {
|
|
292
|
+
scripts.lint = "eslint src/";
|
|
293
|
+
}
|
|
243
294
|
const packageJson = {
|
|
244
295
|
name: config.name,
|
|
245
296
|
private: true,
|
|
246
297
|
version: "0.0.1",
|
|
247
298
|
type: "module",
|
|
248
|
-
scripts
|
|
249
|
-
develop: `shovel develop src/app.${ext} --platform ${config.platform}`,
|
|
250
|
-
build: `shovel build src/app.${ext} --platform ${config.platform}`,
|
|
251
|
-
start: startCmd
|
|
252
|
-
},
|
|
299
|
+
scripts,
|
|
253
300
|
dependencies,
|
|
254
|
-
devDependencies
|
|
255
|
-
"@types/node": "^18.0.0",
|
|
256
|
-
typescript: "^5.0.0"
|
|
257
|
-
} : {}
|
|
301
|
+
devDependencies
|
|
258
302
|
};
|
|
259
303
|
await writeFile(
|
|
260
304
|
join(projectPath, "package.json"),
|
|
261
305
|
JSON.stringify(packageJson, null, 2)
|
|
262
306
|
);
|
|
263
|
-
const
|
|
264
|
-
|
|
307
|
+
const appResult = generateAppFile(config);
|
|
308
|
+
if (typeof appResult === "string") {
|
|
309
|
+
await writeFile(join(projectPath, `src/app.${ext}`), appResult);
|
|
310
|
+
} else {
|
|
311
|
+
for (const [filename, content] of Object.entries(appResult)) {
|
|
312
|
+
await writeFile(join(projectPath, `src/${filename}`), content);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
265
315
|
if (config.typescript) {
|
|
266
316
|
const compilerOptions = {
|
|
267
317
|
target: "ES2022",
|
|
@@ -274,12 +324,15 @@ async function createProject(config, projectPath) {
|
|
|
274
324
|
forceConsistentCasingInFileNames: true,
|
|
275
325
|
lib: ["ES2022", "WebWorker"]
|
|
276
326
|
};
|
|
277
|
-
if (config.uiFramework === "crank") {
|
|
327
|
+
if (config.uiFramework === "crank" && config.useJSX) {
|
|
278
328
|
compilerOptions.jsx = "react-jsx";
|
|
279
329
|
compilerOptions.jsxImportSource = "@b9g/crank";
|
|
280
330
|
}
|
|
281
331
|
const tsConfig = {
|
|
282
|
-
compilerOptions
|
|
332
|
+
compilerOptions: {
|
|
333
|
+
...compilerOptions,
|
|
334
|
+
types: ["@b9g/platform/globals"]
|
|
335
|
+
},
|
|
283
336
|
include: ["src/**/*"],
|
|
284
337
|
exclude: ["node_modules", "dist"]
|
|
285
338
|
};
|
|
@@ -287,18 +340,16 @@ async function createProject(config, projectPath) {
|
|
|
287
340
|
join(projectPath, "tsconfig.json"),
|
|
288
341
|
JSON.stringify(tsConfig, null, 2)
|
|
289
342
|
);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
activate: ExtendableEvent;
|
|
299
|
-
}
|
|
343
|
+
}
|
|
344
|
+
if (isCrank) {
|
|
345
|
+
const eslintConfig = `import js from "@eslint/js";
|
|
346
|
+
|
|
347
|
+
export default [
|
|
348
|
+
js.configs.recommended,
|
|
349
|
+
{ ignores: ["dist/"] },
|
|
350
|
+
];
|
|
300
351
|
`;
|
|
301
|
-
await writeFile(join(projectPath, "
|
|
352
|
+
await writeFile(join(projectPath, "eslint.config.js"), eslintConfig);
|
|
302
353
|
}
|
|
303
354
|
const readme = generateReadme(config);
|
|
304
355
|
await writeFile(join(projectPath, "README.md"), readme);
|
|
@@ -600,16 +651,50 @@ ${css}
|
|
|
600
651
|
}
|
|
601
652
|
function generateStaticSiteCrank(config) {
|
|
602
653
|
const t = config.typescript;
|
|
603
|
-
|
|
654
|
+
const ext = config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
655
|
+
if (config.useJSX) {
|
|
656
|
+
return {
|
|
657
|
+
[`server.${ext}`]: `import {renderer} from "@b9g/crank/html";
|
|
658
|
+
import {Router} from "@b9g/router";
|
|
659
|
+
import {Page} from "./components";
|
|
604
660
|
|
|
605
|
-
|
|
606
|
-
// Server-rendered HTML with JSX components
|
|
661
|
+
const router = new Router();
|
|
607
662
|
|
|
608
|
-
|
|
663
|
+
router.route("/").get(async () => {
|
|
664
|
+
const html = await renderer.render(
|
|
665
|
+
<Page title="Home">
|
|
666
|
+
<h1>Welcome to ${config.name}</h1>
|
|
667
|
+
<p>Edit <code>src/server.${ext}</code> to get started.</p>
|
|
668
|
+
<p><a href="/about">About</a></p>
|
|
669
|
+
</Page>
|
|
670
|
+
);
|
|
671
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
672
|
+
headers: { "Content-Type": "text/html" },
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
router.route("/about").get(async () => {
|
|
677
|
+
const html = await renderer.render(
|
|
678
|
+
<Page title="About">
|
|
679
|
+
<h1>About</h1>
|
|
680
|
+
<p>This is a static site built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
681
|
+
<p><a href="/">Home</a></p>
|
|
682
|
+
</Page>
|
|
683
|
+
);
|
|
684
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
685
|
+
headers: { "Content-Type": "text/html" },
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
self.addEventListener("fetch", (event) => {
|
|
690
|
+
event.respondWith(router.handle(event.request));
|
|
691
|
+
});
|
|
692
|
+
`,
|
|
693
|
+
[`components.${ext}`]: `const css = \`
|
|
609
694
|
${css}
|
|
610
695
|
\`;
|
|
611
696
|
|
|
612
|
-
function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
697
|
+
export function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
613
698
|
return (
|
|
614
699
|
<html lang="en">
|
|
615
700
|
<head>
|
|
@@ -624,40 +709,70 @@ function Page({title, children}${t ? ": {title: string, children: unknown}" : ""
|
|
|
624
709
|
</html>
|
|
625
710
|
);
|
|
626
711
|
}
|
|
712
|
+
`
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
[`server.${ext}`]: `import {jsx} from "@b9g/crank/standalone";
|
|
717
|
+
import {renderer} from "@b9g/crank/html";
|
|
718
|
+
import {Router} from "@b9g/router";
|
|
719
|
+
import {Page} from "./components";
|
|
627
720
|
|
|
628
|
-
|
|
629
|
-
event.respondWith(handleRequest(event.request));
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
async function handleRequest(request${t ? ": Request" : ""})${t ? ": Promise<Response>" : ""} {
|
|
633
|
-
const url = new URL(request.url);
|
|
634
|
-
let html${t ? ": string" : ""};
|
|
721
|
+
const router = new Router();
|
|
635
722
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
<p>This is a static site built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
649
|
-
<p><a href="/">Home</a></p>
|
|
650
|
-
</Page>
|
|
651
|
-
);
|
|
652
|
-
} else {
|
|
653
|
-
return new Response("Not Found", { status: 404 });
|
|
654
|
-
}
|
|
723
|
+
router.route("/").get(async () => {
|
|
724
|
+
const html = await renderer.render(jsx\`
|
|
725
|
+
<\${Page} title="Home">
|
|
726
|
+
<h1>Welcome to ${config.name}</h1>
|
|
727
|
+
<p>Edit <code>src/server.${ext}</code> to get started.</p>
|
|
728
|
+
<p><a href="/about">About</a></p>
|
|
729
|
+
</\${Page}>
|
|
730
|
+
\`);
|
|
731
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
732
|
+
headers: { "Content-Type": "text/html" },
|
|
733
|
+
});
|
|
734
|
+
});
|
|
655
735
|
|
|
736
|
+
router.route("/about").get(async () => {
|
|
737
|
+
const html = await renderer.render(jsx\`
|
|
738
|
+
<\${Page} title="About">
|
|
739
|
+
<h1>About</h1>
|
|
740
|
+
<p>This is a static site built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
741
|
+
<p><a href="/">Home</a></p>
|
|
742
|
+
</\${Page}>
|
|
743
|
+
\`);
|
|
656
744
|
return new Response("<!DOCTYPE html>" + html, {
|
|
657
745
|
headers: { "Content-Type": "text/html" },
|
|
658
746
|
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
self.addEventListener("fetch", (event) => {
|
|
750
|
+
event.respondWith(router.handle(event.request));
|
|
751
|
+
});
|
|
752
|
+
`,
|
|
753
|
+
[`components.${ext}`]: `import {jsx} from "@b9g/crank/standalone";
|
|
754
|
+
|
|
755
|
+
const css = \`
|
|
756
|
+
${css}
|
|
757
|
+
\`;
|
|
758
|
+
|
|
759
|
+
export function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
760
|
+
return jsx\`
|
|
761
|
+
<html lang="en">
|
|
762
|
+
<head>
|
|
763
|
+
<meta charset="UTF-8" />
|
|
764
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
765
|
+
<title>\${title} - ${config.name}</title>
|
|
766
|
+
<style>\${css}</style>
|
|
767
|
+
</head>
|
|
768
|
+
<body>
|
|
769
|
+
<main>\${children}</main>
|
|
770
|
+
</body>
|
|
771
|
+
</html>
|
|
772
|
+
\`;
|
|
659
773
|
}
|
|
660
|
-
|
|
774
|
+
`
|
|
775
|
+
};
|
|
661
776
|
}
|
|
662
777
|
function generateFullStack(config) {
|
|
663
778
|
switch (config.uiFramework) {
|
|
@@ -895,18 +1010,69 @@ self.addEventListener("fetch", (event) => {
|
|
|
895
1010
|
}
|
|
896
1011
|
function generateFullStackCrank(config) {
|
|
897
1012
|
const t = config.typescript;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1013
|
+
const ext = config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
1014
|
+
if (config.useJSX) {
|
|
1015
|
+
return {
|
|
1016
|
+
[`server.${ext}`]: `import {renderer} from "@b9g/crank/html";
|
|
1017
|
+
import {Router} from "@b9g/router";
|
|
1018
|
+
import {logger} from "@b9g/router/middleware";
|
|
1019
|
+
import {Page} from "./components";
|
|
901
1020
|
|
|
902
1021
|
const router = new Router();
|
|
903
1022
|
router.use(logger());
|
|
904
1023
|
|
|
905
|
-
|
|
1024
|
+
// API routes
|
|
1025
|
+
router.route("/api/hello").get(() => {
|
|
1026
|
+
return Response.json({
|
|
1027
|
+
message: "Hello from the API!",
|
|
1028
|
+
timestamp: new Date().toISOString(),
|
|
1029
|
+
});
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
router.route("/api/echo").post(async (req) => {
|
|
1033
|
+
const body = await req.json();
|
|
1034
|
+
return Response.json({ echo: body });
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// HTML pages
|
|
1038
|
+
router.route("/").get(async () => {
|
|
1039
|
+
const html = await renderer.render(
|
|
1040
|
+
<Page title="Home">
|
|
1041
|
+
<h1>Welcome to ${config.name}</h1>
|
|
1042
|
+
<p>Edit <code>src/server.${ext}</code> to get started.</p>
|
|
1043
|
+
<ul>
|
|
1044
|
+
<li><a href="/about">About</a></li>
|
|
1045
|
+
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
1046
|
+
</ul>
|
|
1047
|
+
</Page>
|
|
1048
|
+
);
|
|
1049
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
1050
|
+
headers: { "Content-Type": "text/html" },
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
router.route("/about").get(async () => {
|
|
1055
|
+
const html = await renderer.render(
|
|
1056
|
+
<Page title="About">
|
|
1057
|
+
<h1>About</h1>
|
|
1058
|
+
<p>This is a full-stack app built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
1059
|
+
<p><a href="/">Home</a></p>
|
|
1060
|
+
</Page>
|
|
1061
|
+
);
|
|
1062
|
+
return new Response("<!DOCTYPE html>" + html, {
|
|
1063
|
+
headers: { "Content-Type": "text/html" },
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
self.addEventListener("fetch", (event) => {
|
|
1068
|
+
event.respondWith(router.handle(event.request));
|
|
1069
|
+
});
|
|
1070
|
+
`,
|
|
1071
|
+
[`components.${ext}`]: `const css = \`
|
|
906
1072
|
${css}
|
|
907
1073
|
\`;
|
|
908
1074
|
|
|
909
|
-
function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
1075
|
+
export function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
910
1076
|
return (
|
|
911
1077
|
<html lang="en">
|
|
912
1078
|
<head>
|
|
@@ -921,6 +1087,18 @@ function Page({title, children}${t ? ": {title: string, children: unknown}" : ""
|
|
|
921
1087
|
</html>
|
|
922
1088
|
);
|
|
923
1089
|
}
|
|
1090
|
+
`
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
[`server.${ext}`]: `import {jsx} from "@b9g/crank/standalone";
|
|
1095
|
+
import {renderer} from "@b9g/crank/html";
|
|
1096
|
+
import {Router} from "@b9g/router";
|
|
1097
|
+
import {logger} from "@b9g/router/middleware";
|
|
1098
|
+
import {Page} from "./components";
|
|
1099
|
+
|
|
1100
|
+
const router = new Router();
|
|
1101
|
+
router.use(logger());
|
|
924
1102
|
|
|
925
1103
|
// API routes
|
|
926
1104
|
router.route("/api/hello").get(() => {
|
|
@@ -937,29 +1115,29 @@ router.route("/api/echo").post(async (req) => {
|
|
|
937
1115
|
|
|
938
1116
|
// HTML pages
|
|
939
1117
|
router.route("/").get(async () => {
|
|
940
|
-
const html = await renderer.render(
|
|
941
|
-
|
|
1118
|
+
const html = await renderer.render(jsx\`
|
|
1119
|
+
<\${Page} title="Home">
|
|
942
1120
|
<h1>Welcome to ${config.name}</h1>
|
|
943
|
-
<p>Edit <code>src/
|
|
1121
|
+
<p>Edit <code>src/server.${ext}</code> to get started.</p>
|
|
944
1122
|
<ul>
|
|
945
1123
|
<li><a href="/about">About</a></li>
|
|
946
1124
|
<li><a href="/api/hello">API: /api/hello</a></li>
|
|
947
1125
|
</ul>
|
|
948
|
-
|
|
949
|
-
);
|
|
1126
|
+
</\${Page}>
|
|
1127
|
+
\`);
|
|
950
1128
|
return new Response("<!DOCTYPE html>" + html, {
|
|
951
1129
|
headers: { "Content-Type": "text/html" },
|
|
952
1130
|
});
|
|
953
1131
|
});
|
|
954
1132
|
|
|
955
1133
|
router.route("/about").get(async () => {
|
|
956
|
-
const html = await renderer.render(
|
|
957
|
-
|
|
1134
|
+
const html = await renderer.render(jsx\`
|
|
1135
|
+
<\${Page} title="About">
|
|
958
1136
|
<h1>About</h1>
|
|
959
1137
|
<p>This is a full-stack app built with <strong>Shovel</strong> and <strong>Crank.js</strong>.</p>
|
|
960
1138
|
<p><a href="/">Home</a></p>
|
|
961
|
-
|
|
962
|
-
);
|
|
1139
|
+
</\${Page}>
|
|
1140
|
+
\`);
|
|
963
1141
|
return new Response("<!DOCTYPE html>" + html, {
|
|
964
1142
|
headers: { "Content-Type": "text/html" },
|
|
965
1143
|
});
|
|
@@ -968,7 +1146,30 @@ router.route("/about").get(async () => {
|
|
|
968
1146
|
self.addEventListener("fetch", (event) => {
|
|
969
1147
|
event.respondWith(router.handle(event.request));
|
|
970
1148
|
});
|
|
971
|
-
|
|
1149
|
+
`,
|
|
1150
|
+
[`components.${ext}`]: `import {jsx} from "@b9g/crank/standalone";
|
|
1151
|
+
|
|
1152
|
+
const css = \`
|
|
1153
|
+
${css}
|
|
1154
|
+
\`;
|
|
1155
|
+
|
|
1156
|
+
export function Page({title, children}${t ? ": {title: string, children: unknown}" : ""}) {
|
|
1157
|
+
return jsx\`
|
|
1158
|
+
<html lang="en">
|
|
1159
|
+
<head>
|
|
1160
|
+
<meta charset="UTF-8" />
|
|
1161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
1162
|
+
<title>\${title} - ${config.name}</title>
|
|
1163
|
+
<style>\${css}</style>
|
|
1164
|
+
</head>
|
|
1165
|
+
<body>
|
|
1166
|
+
<main>\${children}</main>
|
|
1167
|
+
</body>
|
|
1168
|
+
</html>
|
|
1169
|
+
\`;
|
|
1170
|
+
}
|
|
1171
|
+
`
|
|
1172
|
+
};
|
|
972
1173
|
}
|
|
973
1174
|
function generateReadme(config) {
|
|
974
1175
|
const templateDescriptions = {
|
|
@@ -983,7 +1184,24 @@ function generateReadme(config) {
|
|
|
983
1184
|
alpine: " using [Alpine.js](https://alpinejs.dev)",
|
|
984
1185
|
crank: " using [Crank.js](https://crank.js.org)"
|
|
985
1186
|
};
|
|
986
|
-
const ext = config.uiFramework === "crank" ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
1187
|
+
const ext = config.uiFramework === "crank" && config.useJSX ? config.typescript ? "tsx" : "jsx" : config.typescript ? "ts" : "js";
|
|
1188
|
+
const isCrank = config.uiFramework === "crank";
|
|
1189
|
+
let projectTree;
|
|
1190
|
+
if (isCrank) {
|
|
1191
|
+
projectTree = `${config.name}/
|
|
1192
|
+
\u251C\u2500\u2500 src/
|
|
1193
|
+
\u2502 \u251C\u2500\u2500 server.${ext} # Application entry point
|
|
1194
|
+
\u2502 \u2514\u2500\u2500 components.${ext} # Page components
|
|
1195
|
+
\u251C\u2500\u2500 eslint.config.js
|
|
1196
|
+
\u251C\u2500\u2500 package.json
|
|
1197
|
+
${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md`;
|
|
1198
|
+
} else {
|
|
1199
|
+
projectTree = `${config.name}/
|
|
1200
|
+
\u251C\u2500\u2500 src/
|
|
1201
|
+
\u2502 \u2514\u2500\u2500 app.${ext} # Application entry point
|
|
1202
|
+
\u251C\u2500\u2500 package.json
|
|
1203
|
+
${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md`;
|
|
1204
|
+
}
|
|
987
1205
|
return `# ${config.name}
|
|
988
1206
|
|
|
989
1207
|
${templateDescriptions[config.template]}${frameworkDescriptions[config.uiFramework]}, built with [Shovel](https://github.com/bikeshaving/shovel).
|
|
@@ -1001,16 +1219,12 @@ Open http://localhost:7777
|
|
|
1001
1219
|
|
|
1002
1220
|
- \`npm run develop\` - Start development server
|
|
1003
1221
|
- \`npm run build\` - Build for production
|
|
1004
|
-
- \`npm start\` - Run production build
|
|
1222
|
+
- \`npm start\` - Run production build${isCrank ? "\n- `npm run lint` - Lint source files" : ""}
|
|
1005
1223
|
|
|
1006
1224
|
## Project Structure
|
|
1007
1225
|
|
|
1008
1226
|
\`\`\`
|
|
1009
|
-
${
|
|
1010
|
-
\u251C\u2500\u2500 src/
|
|
1011
|
-
\u2502 \u2514\u2500\u2500 app.${ext} # Application entry point
|
|
1012
|
-
\u251C\u2500\u2500 package.json
|
|
1013
|
-
${config.typescript ? "\u251C\u2500\u2500 tsconfig.json\n" : ""}\u2514\u2500\u2500 README.md
|
|
1227
|
+
${projectTree}
|
|
1014
1228
|
\`\`\`
|
|
1015
1229
|
|
|
1016
1230
|
## Learn More
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/shovel",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "ServiceWorker-first universal deployment platform. Write ServiceWorker apps once, deploy anywhere (Node/Bun/Cloudflare). Registry-based multi-app orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@b9g/async-context": "^0.2.1",
|
|
18
18
|
"@b9g/cache": "^0.2.2",
|
|
19
|
-
"@b9g/filesystem": "^0.
|
|
19
|
+
"@b9g/filesystem": "^0.2.0",
|
|
20
20
|
"@b9g/http-errors": "^0.2.1",
|
|
21
21
|
"@b9g/node-webworker": "^0.2.1",
|
|
22
22
|
"@b9g/platform": "^0.1.17",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@clack/prompts": "^0.7.0",
|
|
27
27
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
|
28
28
|
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
|
29
|
-
"@logtape/logtape": "^
|
|
29
|
+
"@logtape/logtape": "^2.0.0",
|
|
30
30
|
"commander": "^13.1.0",
|
|
31
31
|
"esbuild": "^0.27.2",
|
|
32
32
|
"mime": "^4.0.4",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@b9g/crank": "^0.7.2",
|
|
38
38
|
"@b9g/libuild": "^0.1.22",
|
|
39
39
|
"@b9g/router": "^0.2.2",
|
|
40
|
-
"@logtape/file": "^
|
|
40
|
+
"@logtape/file": "^2.0.0",
|
|
41
41
|
"@types/bun": "^1.3.4",
|
|
42
42
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
43
43
|
"@typescript-eslint/parser": "^8.0.0",
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { getLogger } from "@logtape/logtape";
|
|
11
11
|
import { resolvePlatform } from "@b9g/platform";
|
|
12
12
|
import { networkInterfaces } from "os";
|
|
13
|
+
import { exec } from "child_process";
|
|
13
14
|
var logger = getLogger(["shovel", "develop"]);
|
|
14
15
|
function getDisplayUrls(host, port) {
|
|
15
16
|
const urls = {
|
|
@@ -56,7 +57,8 @@ async function developCommand(entrypoint, options, config) {
|
|
|
56
57
|
const platformModule = await loadPlatformModule(platformName);
|
|
57
58
|
const platformESBuildConfig = platformModule.getESBuildConfig();
|
|
58
59
|
let devServer = null;
|
|
59
|
-
const
|
|
60
|
+
const localUrl = `http://localhost:${port}`;
|
|
61
|
+
const SHORTCUTS_HELP = "Ctrl+R (reload) Ctrl+O (open) Ctrl+C (quit) ? (help)";
|
|
60
62
|
const startOrReloadServer = async (workerPath) => {
|
|
61
63
|
if (!devServer) {
|
|
62
64
|
devServer = await platformModule.createDevServer({
|
|
@@ -117,6 +119,10 @@ async function developCommand(entrypoint, options, config) {
|
|
|
117
119
|
process.stdin.setRawMode(true);
|
|
118
120
|
process.stdin.resume();
|
|
119
121
|
process.stdin.setEncoding("utf8");
|
|
122
|
+
process.on("SIGCONT", () => {
|
|
123
|
+
process.stdin.setRawMode(true);
|
|
124
|
+
process.stdin.resume();
|
|
125
|
+
});
|
|
120
126
|
process.stdin.on("data", async (key) => {
|
|
121
127
|
switch (key) {
|
|
122
128
|
case "":
|
|
@@ -129,9 +135,33 @@ async function developCommand(entrypoint, options, config) {
|
|
|
129
135
|
case "":
|
|
130
136
|
await shutdown("SIGINT");
|
|
131
137
|
break;
|
|
138
|
+
case "":
|
|
139
|
+
process.stdin.setRawMode(false);
|
|
140
|
+
process.kill(process.pid, "SIGTSTP");
|
|
141
|
+
break;
|
|
142
|
+
case "":
|
|
143
|
+
await shutdown("SIGQUIT");
|
|
144
|
+
break;
|
|
145
|
+
case "":
|
|
146
|
+
await shutdown("SIGINT");
|
|
147
|
+
break;
|
|
148
|
+
case "": {
|
|
149
|
+
const cmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
150
|
+
exec(`${cmd} ${localUrl}`);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "\r":
|
|
154
|
+
process.stdout.write("\n");
|
|
155
|
+
break;
|
|
156
|
+
case "\x7F":
|
|
157
|
+
process.stdout.write("\b \b");
|
|
158
|
+
break;
|
|
132
159
|
case "?":
|
|
133
160
|
logger.info(SHORTCUTS_HELP);
|
|
134
161
|
break;
|
|
162
|
+
default:
|
|
163
|
+
process.stdout.write(key);
|
|
164
|
+
break;
|
|
135
165
|
}
|
|
136
166
|
});
|
|
137
167
|
}
|