@webjsdev/cli 0.10.39 → 0.10.40
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/lib/app-tasks.js
CHANGED
|
@@ -4,15 +4,20 @@ import { join } from 'node:path';
|
|
|
4
4
|
/**
|
|
5
5
|
* Read the dev/start task orchestration from an app's `package.json` `"webjs"`
|
|
6
6
|
* block (#550). This is what lets `webjs dev` / `webjs start` behave identically
|
|
7
|
-
* to `npm run dev` / `npm run start`: the orchestration (Tailwind
|
|
8
|
-
* `db migrate` before
|
|
7
|
+
* to `npm run dev` / `npm run start`: the orchestration (a Tailwind compile, a
|
|
8
|
+
* `db migrate` before boot) moves OUT of `concurrently` + `pre*` npm hooks
|
|
9
9
|
* and INTO the framework primitive, so a bare `webjs dev` is not a degraded run.
|
|
10
10
|
*
|
|
11
|
+
* Reads only `before` / `parallel` here (the CLI-run tasks). The dev-only
|
|
12
|
+
* `regenerate` key (#967, on-request rebuilds of a stale served output like
|
|
13
|
+
* `public/tailwind.css`) is read by the SERVER, not this reader, so the scaffold
|
|
14
|
+
* keeps its static CSS fresh WITHOUT a `--watch` under `parallel`.
|
|
15
|
+
*
|
|
11
16
|
* Shape:
|
|
12
17
|
* "webjs": {
|
|
13
18
|
* "dev": {
|
|
14
|
-
* "before": ["webjs db migrate"]
|
|
15
|
-
*
|
|
19
|
+
* "before": ["webjs db migrate", "tailwindcss -i ./public/input.css -o ./public/tailwind.css --minify"]
|
|
20
|
+
* // dev.regenerate keeps the CSS fresh on request (server-read, see dev-regenerate.js)
|
|
16
21
|
* },
|
|
17
22
|
* "start": { "before": ["webjs db migrate"] }
|
|
18
23
|
* }
|
|
@@ -20,9 +25,10 @@ import { join } from 'node:path';
|
|
|
20
25
|
* `before` commands run sequentially to completion BEFORE the server boots (the
|
|
21
26
|
* old `predev` / `prestart` hooks: a one-shot `webjs db migrate`).
|
|
22
27
|
* `parallel` (dev only) commands run as long-lived child processes ALONGSIDE the
|
|
23
|
-
* server (the old `concurrently` watchers
|
|
24
|
-
* (never undefined) so callers iterate
|
|
25
|
-
* config yields empty arrays so a plain app
|
|
28
|
+
* server (the old `concurrently` watchers), for a genuinely long-lived side
|
|
29
|
+
* process. Returns normalized arrays (never undefined) so callers iterate
|
|
30
|
+
* without guards, and a missing/empty config yields empty arrays so a plain app
|
|
31
|
+
* runs `webjs dev`/`start` unchanged.
|
|
26
32
|
*
|
|
27
33
|
* Pure (reads one file, never spawns / prints / exits) so it is unit-testable
|
|
28
34
|
* without a process, matching `lib/port.js` and `lib/dev-supervisor.js`.
|
package/lib/create.js
CHANGED
|
@@ -348,12 +348,11 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
348
348
|
// (not the browser runtime), so it renders styled with JavaScript off. The
|
|
349
349
|
// compiler is a node-shebang CLI, so a Bun app (whose Dockerfile is a node-less
|
|
350
350
|
// `oven/bun:1` image, #595) must run it under Bun via `bun --bun`; a Node app
|
|
351
|
-
// runs it directly (the before /
|
|
351
|
+
// runs it directly (the before / regenerate steps get node_modules/.bin on PATH
|
|
352
352
|
// via envWithLocalBin). Deliberately NOT `npm run css:build` in the hooks: the
|
|
353
353
|
// Bun image has no npm, so that step would exit 127 and abort the boot.
|
|
354
354
|
const twBin = isBun ? 'bun --bun tailwindcss' : 'tailwindcss';
|
|
355
355
|
const cssBuildCmd = `${twBin} -i ./public/input.css -o ./public/tailwind.css --minify`;
|
|
356
|
-
const cssWatchCmd = `${twBin} -i ./public/input.css -o ./public/tailwind.css --watch`;
|
|
357
356
|
const appDir = join(cwd, name);
|
|
358
357
|
if (existsSync(appDir)) {
|
|
359
358
|
console.error(`Error: directory '${name}' already exists.`);
|
|
@@ -489,15 +488,31 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
489
488
|
// migrate` (idempotent, a no-op when the db is current), so a freshly
|
|
490
489
|
// generated migration is applied without a manual step (#725). For a UI
|
|
491
490
|
// template it ALSO compiles Tailwind in `before` so a freshly cloned app is
|
|
492
|
-
// styled on the very first boot with no manual step
|
|
493
|
-
//
|
|
494
|
-
// is the runtime-aware `cssBuildCmd` (a Bun app runs it under Bun, since its
|
|
491
|
+
// styled on the very first boot with no manual step. The compile command is
|
|
492
|
+
// the runtime-aware `cssBuildCmd` (a Bun app runs it under Bun, since its
|
|
495
493
|
// image has no npm or Node), NOT `npm run css:build`. The api template has no
|
|
496
494
|
// CSS, so it gets neither.
|
|
495
|
+
//
|
|
496
|
+
// In dev the static public/tailwind.css is kept fresh by `dev.regenerate`
|
|
497
|
+
// (#967), NOT a background `tailwindcss --watch`. A watch that dies mid-
|
|
498
|
+
// session or never starts serves stale/missing CSS with no error (a newly
|
|
499
|
+
// added utility class has no backing rule, so the app renders unstyled
|
|
500
|
+
// locally while prod is fine). `regenerate` instead recompiles ON REQUEST
|
|
501
|
+
// when the output is older than a source (or missing): the framework rebuilds
|
|
502
|
+
// it before serving `/public/tailwind.css`, so there is no watch process to
|
|
503
|
+
// die and no staleness window. Same `cssBuildCmd` as prod, so dev and prod
|
|
504
|
+
// resolve classes identically (nothing to diverge). `inputs` mirrors the
|
|
505
|
+
// input.css @source globs (the dirs Tailwind scans for classes).
|
|
497
506
|
webjs: {
|
|
498
507
|
dev: {
|
|
499
508
|
before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd],
|
|
500
|
-
...(isApi ? {} : {
|
|
509
|
+
...(isApi ? {} : {
|
|
510
|
+
regenerate: [{
|
|
511
|
+
output: 'public/tailwind.css',
|
|
512
|
+
command: cssBuildCmd,
|
|
513
|
+
inputs: ['app', 'components', 'modules', 'lib', 'public/input.css'],
|
|
514
|
+
}],
|
|
515
|
+
}),
|
|
501
516
|
},
|
|
502
517
|
start: { before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd] },
|
|
503
518
|
},
|
|
@@ -1226,8 +1241,9 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1226
1241
|
})();
|
|
1227
1242
|
</script>
|
|
1228
1243
|
<!-- Tailwind: a STATIC stylesheet compiled from public/input.css to
|
|
1229
|
-
public/tailwind.css by css:build
|
|
1230
|
-
|
|
1244
|
+
public/tailwind.css by css:build (run automatically by the dev and start
|
|
1245
|
+
tasks; in dev it is also recompiled on request when a source changes, so
|
|
1246
|
+
it never goes stale). A real stylesheet, so the app is fully styled with
|
|
1231
1247
|
JavaScript DISABLED (no in-browser compile). -->
|
|
1232
1248
|
<link rel="stylesheet" href="/public/tailwind.css">
|
|
1233
1249
|
<style>
|
package/lib/doctor.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* fails CI because npm was briefly unreachable is worse than useless.
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import { existsSync, statSync } from 'node:fs';
|
|
39
|
+
import { existsSync, statSync, readdirSync } from 'node:fs';
|
|
40
40
|
import { readFile } from 'node:fs/promises';
|
|
41
41
|
import { join, relative } from 'node:path';
|
|
42
42
|
import { createRequire } from 'node:module';
|
|
@@ -802,7 +802,7 @@ function checkGitHook(appDir) {
|
|
|
802
802
|
* Advisory (#646): name why a page/layout SHIPS its module to the browser
|
|
803
803
|
* instead of being elided. A page/layout that is a pure carrier (import-only
|
|
804
804
|
* #605 / inert #179) stays out of the browser; one that ships whole is pinned
|
|
805
|
-
* by a specific client-effecting NON-component
|
|
805
|
+
* by a specific client-effecting NON-component on a component-free path from it, #963 (a util touching
|
|
806
806
|
* a client global, a module-scope side effect, a bare side-effect import) or by
|
|
807
807
|
* its own client work. This turns that invisible #605/#179 regression into a
|
|
808
808
|
* named line. WARN only: a page legitimately MAY ship, and the analyser is
|
|
@@ -881,6 +881,89 @@ async function checkScaffoldDesign(appDir) {
|
|
|
881
881
|
};
|
|
882
882
|
}
|
|
883
883
|
|
|
884
|
+
// Directories never worth walking for the CSS-freshness advisory (mirrors
|
|
885
|
+
// dev-regenerate's IGNORE_DIRS): build output, deps, VCS + framework caches.
|
|
886
|
+
const FRESHNESS_IGNORE = new Set(['node_modules', '.git', '.webjs', 'dist', '.next', 'coverage']);
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Newest mtime (ms) of any FILE under a path (a file's own, or the max over the
|
|
890
|
+
* files in a directory tree, skipping dependencies / dotfiles). Directory-node
|
|
891
|
+
* mtimes are NOT counted, matching dev-regenerate's walker: a content edit only
|
|
892
|
+
* shows through the file mtime, and a directory mtime is a flaky moving target.
|
|
893
|
+
* A missing path is 0. Best-effort: never throws.
|
|
894
|
+
* @param {string} abs
|
|
895
|
+
* @returns {number}
|
|
896
|
+
*/
|
|
897
|
+
function newestMtimeMs(abs) {
|
|
898
|
+
let st;
|
|
899
|
+
try { st = statSync(abs); } catch { return 0; }
|
|
900
|
+
if (!st.isDirectory()) return st.mtimeMs;
|
|
901
|
+
let newest = 0;
|
|
902
|
+
let entries;
|
|
903
|
+
try { entries = readdirSync(abs, { withFileTypes: true }); } catch { return newest; }
|
|
904
|
+
for (const e of entries) {
|
|
905
|
+
if (e.name.startsWith('.') || FRESHNESS_IGNORE.has(e.name)) continue;
|
|
906
|
+
// Skip symlinks: following one can cycle into unbounded recursion (a stack
|
|
907
|
+
// overflow here) or escape into node_modules. Same tradeoff as the server
|
|
908
|
+
// walker in dev-regenerate.js.
|
|
909
|
+
if (e.isSymbolicLink()) continue;
|
|
910
|
+
const m = newestMtimeMs(join(abs, e.name));
|
|
911
|
+
if (m > newest) newest = m;
|
|
912
|
+
}
|
|
913
|
+
return newest;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* ADVISORY: a declared `webjs.dev.regenerate` output is STALE on disk (a source
|
|
918
|
+
* is newer than the committed/built output). In DEV the framework recompiles it
|
|
919
|
+
* on request (#967), so this never bites locally, but the check is the explicit
|
|
920
|
+
* dev/prod PARITY backstop: it catches a stale `public/tailwind.css` that would
|
|
921
|
+
* be served as-is by `webjs start` (prod does NOT recompile on request) or
|
|
922
|
+
* committed into the repo. WARN-level: the fix is a one-line rebuild, and a
|
|
923
|
+
* missing output (a fresh clone before the first `css:build`) is not this app's
|
|
924
|
+
* bug to hard-fail on.
|
|
925
|
+
* @param {string} appDir
|
|
926
|
+
* @returns {Promise<DoctorResult>}
|
|
927
|
+
*/
|
|
928
|
+
async function checkStaticAssetFreshness(appDir) {
|
|
929
|
+
const name = 'Static build outputs (dev.regenerate freshness)';
|
|
930
|
+
let pkg;
|
|
931
|
+
try {
|
|
932
|
+
pkg = JSON.parse(await readFile(join(appDir, 'package.json'), 'utf8'));
|
|
933
|
+
} catch {
|
|
934
|
+
return { name, status: 'pass', message: 'no package.json to analyse' };
|
|
935
|
+
}
|
|
936
|
+
const rules = pkg && pkg.webjs && pkg.webjs.dev ? pkg.webjs.dev.regenerate : null;
|
|
937
|
+
if (!Array.isArray(rules) || rules.length === 0) {
|
|
938
|
+
return { name, status: 'pass', message: 'no webjs.dev.regenerate rules declared' };
|
|
939
|
+
}
|
|
940
|
+
const stale = [];
|
|
941
|
+
for (const rule of rules) {
|
|
942
|
+
if (!rule || typeof rule.output !== 'string') continue;
|
|
943
|
+
const output = rule.output.replace(/^\/+/, '');
|
|
944
|
+
const outMtime = newestMtimeMs(join(appDir, output));
|
|
945
|
+
if (outMtime === 0) continue; // missing output: not a staleness fail (built on first boot)
|
|
946
|
+
let newestSrc = 0;
|
|
947
|
+
for (const inp of Array.isArray(rule.inputs) ? rule.inputs : []) {
|
|
948
|
+
const m = newestMtimeMs(join(appDir, inp));
|
|
949
|
+
if (m > newestSrc) newestSrc = m;
|
|
950
|
+
}
|
|
951
|
+
if (newestSrc > outMtime) stale.push({ output, command: rule.command });
|
|
952
|
+
}
|
|
953
|
+
if (stale.length === 0) {
|
|
954
|
+
return { name, status: 'pass', message: 'every declared build output is up to date with its sources' };
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
name,
|
|
958
|
+
status: 'warn',
|
|
959
|
+
message:
|
|
960
|
+
`${stale.length} static build output(s) are older than a source file:\n` +
|
|
961
|
+
stale.map((s) => ` ${s.output} (rebuild: ${s.command})`).join('\n') +
|
|
962
|
+
'\n In dev the framework recompiles these on request, so this only bites a `webjs start` (prod) or a committed stale file.',
|
|
963
|
+
fix: 'Rebuild the output(s) with the command shown (e.g. `npm run css:build`) before deploying or committing. `webjs dev` regenerates them on request automatically.',
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
884
967
|
/**
|
|
885
968
|
* Probe whether `@webjsdev/core` resolves from `appDir`. Node resolution is
|
|
886
969
|
* directory-relative, so this must probe FROM the app (not the CLI's own
|
|
@@ -972,6 +1055,7 @@ export async function runDoctorChecks(appDir, opts = {}) {
|
|
|
972
1055
|
Promise.resolve(checkGitHook(appDir)),
|
|
973
1056
|
checkElisionCarriers(appDir),
|
|
974
1057
|
checkScaffoldDesign(appDir),
|
|
1058
|
+
checkStaticAssetFreshness(appDir),
|
|
975
1059
|
]);
|
|
976
1060
|
return results;
|
|
977
1061
|
}
|
package/package.json
CHANGED
package/templates/AGENTS.md
CHANGED
|
@@ -493,22 +493,32 @@ npm run dev # webjs dev, then serves
|
|
|
493
493
|
|
|
494
494
|
`npm run dev` and `npm start` are the documented entrypoints, and they
|
|
495
495
|
are thin aliases for `webjs dev` / `webjs start`. The start orchestration
|
|
496
|
-
(applying migrations, and
|
|
497
|
-
|
|
498
|
-
`webjs dev` / `webjs start`:
|
|
496
|
+
(applying migrations, and compiling Tailwind) lives in the `webjs` block
|
|
497
|
+
of `package.json` and runs INSIDE `webjs dev` / `webjs start`:
|
|
499
498
|
|
|
500
499
|
```jsonc
|
|
501
500
|
"webjs": {
|
|
502
|
-
"dev": {
|
|
503
|
-
|
|
501
|
+
"dev": {
|
|
502
|
+
"before": ["webjs db migrate", "tailwindcss -i ./public/input.css -o ./public/tailwind.css --minify"],
|
|
503
|
+
"regenerate": [
|
|
504
|
+
{ "output": "public/tailwind.css",
|
|
505
|
+
"command": "tailwindcss -i ./public/input.css -o ./public/tailwind.css --minify",
|
|
506
|
+
"inputs": ["app", "components", "modules", "lib", "public/input.css"] }
|
|
507
|
+
]
|
|
508
|
+
},
|
|
509
|
+
"start": { "before": ["webjs db migrate", "tailwindcss -i ./public/input.css -o ./public/tailwind.css --minify"] }
|
|
504
510
|
}
|
|
505
511
|
```
|
|
506
512
|
|
|
507
513
|
Both `dev` and `start` apply pending migrations via `webjs db migrate`
|
|
508
|
-
(idempotent, a no-op when the db is current)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
514
|
+
(idempotent, a no-op when the db is current) and compile the static
|
|
515
|
+
`public/tailwind.css`, so a freshly generated migration is applied and
|
|
516
|
+
the app is fully styled with no manual step. In dev the stylesheet is
|
|
517
|
+
then kept fresh by `webjs.dev.regenerate`: the dev server recompiles it
|
|
518
|
+
ON REQUEST whenever a source changes, so a newly added utility class is
|
|
519
|
+
never served stale and there is no `tailwindcss --watch` process that can
|
|
520
|
+
die mid-session (`webjs.dev.parallel` still exists for a genuinely
|
|
521
|
+
long-lived side process, torn down on exit).
|
|
512
522
|
`before` steps run to completion first; a failed `webjs db migrate`
|
|
513
523
|
aborts the boot with a clear message rather than serving a stale schema.
|
|
514
524
|
|
|
@@ -830,7 +840,7 @@ Practical consequences for agents writing webjs code.
|
|
|
830
840
|
| Fetch in `connectedCallback` / `firstUpdated` | Empty first paint (neither hook runs in SSR) | Fetch in the page function, pass as props |
|
|
831
841
|
| `Task` for initial-paint data | SSR ships the pending state, flashes to resolved on hydration | Page function fetch, pass as props, OR an `async render()` in the component (`Task` is fine for client-time async) |
|
|
832
842
|
| Expecting a sync `render()` only | webjs allows `async render() { const d = await getData(); ... }`; SSR bakes the data into the first paint | Use it for request-time server data; `renderFallback()` is the re-fetch loading UI (never first paint); error isolation is automatic |
|
|
833
|
-
| Assuming an `async render()` always ships its module | A bare one (no other client signal) is ELIDED, so it costs zero JS and skips the on-hydration re-fetch, first paint unchanged | Rely on it for a fetch-and-display leaf. `static
|
|
843
|
+
| Assuming an `async render()` always ships its module | A bare one (no other client signal) is ELIDED, so it costs zero JS and skips the on-hydration re-fetch, first paint unchanged | Rely on it for a fetch-and-display leaf. `static interactive = true` forces a ship the analyser would otherwise elide, `static shadow = true` always ships |
|
|
834
844
|
| `window.X` / `document.X` in constructor or `render()` | SSR crash | Move to `connectedCallback` |
|
|
835
845
|
| Top-level `import` of a browser-only library | SSR crash | Dynamic `import()` inside `connectedCallback` |
|
|
836
846
|
| Class-field initializer for a reactive property (`student: Student = {...}`) | Silently breaks reactivity (overwrites the framework accessor) | Pass the shape to the base-class factory `WebComponent({ student: Object })` and set the default in the constructor (flagged by `reactive-props-no-class-field`) |
|
|
@@ -11,12 +11,13 @@ import { WebComponent, html } from '@webjsdev/core';
|
|
|
11
11
|
import { serverGreeting } from '../queries/server-greeting.server.ts';
|
|
12
12
|
|
|
13
13
|
export class ServerClock extends WebComponent {
|
|
14
|
-
// A bare async-render component (no other client signal) is
|
|
15
|
-
// SSR'd HTML is already the complete output
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// A bare async-render component (no other client signal, light DOM) is
|
|
15
|
+
// ELIDED: its SSR'd HTML is already the complete output, so the framework
|
|
16
|
+
// serves it with ZERO JavaScript and skips the redundant on-hydration
|
|
17
|
+
// re-fetch. This is the common fetch-and-display leaf shape. If you ever need
|
|
18
|
+
// to force a component to ship when the analyser would elide it (for
|
|
19
|
+
// interactivity static analysis cannot see, like a dynamically-computed tag
|
|
20
|
+
// string), declare `static interactive = true`.
|
|
20
21
|
async render() {
|
|
21
22
|
const info = await serverGreeting();
|
|
22
23
|
return html`<p class="font-mono text-sm">server rendered this at
|